feat: Implement Club Membership features including activation and retrieval of membership status

- Added command and handler for activating club membership with optional activation code and duration.
- Created response DTO for club membership activation.
- Implemented query and handler to retrieve current user's club membership status.
- Added necessary Protobuf service calls for club membership operations.
- Introduced new queries for retrieving network statistics and network tree structure.
- Enhanced commission queries to fetch user commission payouts and weekly balances.
- Updated application contract context to include new services for club and network memberships.
This commit is contained in:
masoodafar-web
2025-12-04 17:29:34 +03:30
parent bcf2bc2a52
commit 75e446f80f
28 changed files with 1036 additions and 32 deletions

View File

@@ -0,0 +1,22 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership;
/// <summary>
/// فعال‌سازی عضویت باشگاه کاربر جاری (پرداخت 56M)
/// </summary>
public record ActivateMyClubMembershipCommand : IRequest<ActivateMyClubMembershipResponseDto>
{
/// <summary>
/// شناسه پکیج (PackageId)
/// </summary>
public long PackageId { get; init; }
/// <summary>
/// کد فعال‌سازی (اختیاری)
/// </summary>
public string? ActivationCode { get; init; }
/// <summary>
/// مدت زمان عضویت به ماه (پیش‌فرض: 12)
/// </summary>
public int DurationMonths { get; init; } = 12;
}

View File

@@ -0,0 +1,51 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership;
public class ActivateMyClubMembershipCommandHandler : IRequestHandler<ActivateMyClubMembershipCommand, ActivateMyClubMembershipResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public ActivateMyClubMembershipCommandHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<ActivateMyClubMembershipResponseDto> Handle(ActivateMyClubMembershipCommand request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
var grpcRequest = new ActivateClubMembershipRequest
{
UserId = userId,
PackageId = request.PackageId,
DurationMonths = request.DurationMonths
};
// اگر کد فعال‌سازی ارسال شده، اضافه کن
if (!string.IsNullOrEmpty(request.ActivationCode))
{
grpcRequest.ActivationCode = request.ActivationCode;
}
await _context.ClubMemberships.ActivateClubMembershipAsync(
grpcRequest,
cancellationToken: cancellationToken);
var activationDate = DateTime.UtcNow;
var expirationDate = activationDate.AddMonths(request.DurationMonths);
return new ActivateMyClubMembershipResponseDto
{
Success = true,
Message = "عضویت باشگاه با موفقیت فعال شد",
ActivationDate = activationDate,
ExpirationDate = expirationDate,
AmountPaid = 56_000_000 // 56M تومان
};
}
}

View File

@@ -0,0 +1,29 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership;
public class ActivateMyClubMembershipResponseDto
{
/// <summary>
/// آیا فعال‌سازی موفق بود؟
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام نتیجه
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// تاریخ فعال‌سازی
/// </summary>
public DateTime ActivationDate { get; set; }
/// <summary>
/// تاریخ انقضا
/// </summary>
public DateTime ExpirationDate { get; set; }
/// <summary>
/// مبلغ پرداخت شده
/// </summary>
public long AmountPaid { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership;
/// <summary>
/// دریافت وضعیت عضویت باشگاه کاربر جاری
/// </summary>
public record GetMyClubMembershipQuery : IRequest<GetMyClubMembershipResponseDto>;

View File

@@ -0,0 +1,65 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership;
public class GetMyClubMembershipQueryHandler : IRequestHandler<GetMyClubMembershipQuery, GetMyClubMembershipResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetMyClubMembershipQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetMyClubMembershipResponseDto> Handle(GetMyClubMembershipQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
var cmsRequest = new GetClubMembershipRequest
{
UserId = userId
};
var response = await _context.ClubMemberships.GetClubMembershipAsync(cmsRequest, cancellationToken: cancellationToken);
// Fixed: Use ActivatedAt and ExpiresAt from proto (not ActivationDate/ExpirationDate)
var activationDate = response.ActivatedAt?.ToDateTime();
var expirationDate = response.ExpiresAt?.ToDateTime();
var daysRemaining = expirationDate.HasValue
? Math.Max(0, (int)(expirationDate.Value - DateTime.UtcNow).TotalDays)
: 0;
return new GetMyClubMembershipResponseDto
{
UserId = response.UserId,
IsActive = response.IsActive,
ActivationDate = activationDate,
ExpirationDate = expirationDate,
PackageId = response.PackageId,
PackageName = response.PackageName,
ActivationCode = response.ActivationCode,
Status = DetermineStatus(response.IsActive, expirationDate),
DaysRemaining = daysRemaining,
IsTrialPeriod = !response.IsActive && daysRemaining > 0
};
}
private static string DetermineStatus(bool isActive, DateTime? expirationDate)
{
if (!isActive && !expirationDate.HasValue)
return "Inactive";
if (!isActive && expirationDate.HasValue && expirationDate.Value < DateTime.UtcNow)
return "Expired";
if (!isActive)
return "Trial";
return "Active";
}
}

View File

@@ -0,0 +1,54 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership;
public class GetMyClubMembershipResponseDto
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// شناسه پکیج
/// </summary>
public long PackageId { get; set; }
/// <summary>
/// نام پکیج
/// </summary>
public string PackageName { get; set; } = string.Empty;
/// <summary>
/// کد فعال‌سازی
/// </summary>
public string ActivationCode { get; set; } = string.Empty;
/// <summary>
/// وضعیت فعال/غیرفعال
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// تاریخ فعال‌سازی (mapped from ActivatedAt)
/// </summary>
public DateTime? ActivationDate { get; set; }
/// <summary>
/// تاریخ انقضا (mapped from ExpiresAt)
/// </summary>
public DateTime? ExpirationDate { get; set; }
/// <summary>
/// نوع عضویت (Trial/Active/Expired/Inactive)
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// تعداد روزهای باقی‌مانده
/// </summary>
public int DaysRemaining { get; set; }
/// <summary>
/// آیا در دوره آزمایشی است؟
/// </summary>
public bool IsTrialPeriod { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
/// <summary>
/// دریافت لیست پرداخت‌های کمیسیون کاربر جاری
/// </summary>
public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
{
/// <summary>
/// شماره هفته در فرمت ISO (مثال: "2025-W48"، null = همه)
/// </summary>
public string? WeekNumber { get; init; }
/// <summary>
/// وضعیت: 0=Pending, 1=Calculated, 2=Paid, 3=Withdrawn (null = همه)
/// </summary>
public int? Status { get; init; }
/// <summary>
/// شماره صفحه (1-based, internally converted to 0-based PageIndex)
/// </summary>
public int PageNumber { get; init; } = 1;
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; init; } = 20;
}

View File

@@ -0,0 +1,97 @@
using CMSMicroservice.Protobuf.Protos.Commission;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetMyCommissionPayoutsQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetMyCommissionPayoutsResponseDto> Handle(GetMyCommissionPayoutsQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
// Fixed: Use PageIndex (0-based) not PageNumber (1-based), and WeekNumber is string
var cmsRequest = new GetUserCommissionPayoutsRequest
{
UserId = userId, // long? type in proto - just assign directly
PageIndex = Math.Max(0, request.PageNumber - 1), // Convert 1-based to 0-based
PageSize = request.PageSize
};
// WeekNumber is string type in proto - assign directly
if (!string.IsNullOrEmpty(request.WeekNumber))
cmsRequest.WeekNumber = request.WeekNumber;
if (request.Status.HasValue)
cmsRequest.Status = request.Status.Value; // int? type in proto
var response = await _context.Commission.GetUserCommissionPayoutsAsync(cmsRequest, cancellationToken: cancellationToken);
var payouts = response.Models.Select(p => new CommissionPayoutDto
{
Id = p.Id,
WeekNumber = p.WeekNumber,
WeekLabel = $"هفته {p.WeekNumber}",
BalancesEarned = p.BalancesEarned,
TotalAmount = p.TotalAmount,
AmountFormatted = FormatCurrency(p.TotalAmount),
Status = MapStatus(p.Status),
StatusBadgeColor = GetStatusColor(p.Status),
CalculatedDate = p.Created?.ToDateTime() ?? DateTime.UtcNow,
DatePersian = FormatPersianDate(p.Created?.ToDateTime())
}).ToList();
return new GetMyCommissionPayoutsResponseDto
{
Payouts = payouts,
TotalCount = (int)response.MetaData.TotalCount,
PageNumber = request.PageNumber,
PageSize = request.PageSize
};
}
private static string MapStatus(int status)
{
return status switch
{
0 => "Pending",
1 => "Calculated",
2 => "Paid",
3 => "Withdrawn",
_ => "Unknown"
};
}
private static string GetStatusColor(int status)
{
return status switch
{
0 => "warning",
1 => "info",
2 => "success",
3 => "success",
_ => "default"
};
}
private static string FormatCurrency(long amount)
{
return $"{amount:N0} تومان";
}
private static string FormatPersianDate(DateTime? date)
{
if (!date.HasValue) return string.Empty;
// TODO: استفاده از PersianCalendar
return date.Value.ToString("yyyy/MM/dd");
}
}

View File

@@ -0,0 +1,62 @@
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
public class GetMyCommissionPayoutsResponseDto
{
public List<CommissionPayoutDto> Payouts { get; set; } = new();
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
}
public class CommissionPayoutDto
{
/// <summary>
/// شناسه
/// </summary>
public long Id { get; set; }
/// <summary>
/// شماره هفته (e.g., "2024-W45")
/// </summary>
public string WeekNumber { get; set; } = string.Empty;
/// <summary>
/// لیبل هفته (هفته 45 - آذر 1403)
/// </summary>
public string WeekLabel { get; set; } = string.Empty;
/// <summary>
/// تعداد Balance کسب شده
/// </summary>
public int BalancesEarned { get; set; }
/// <summary>
/// مبلغ کل
/// </summary>
public long TotalAmount { get; set; }
/// <summary>
/// مبلغ فرمت شده
/// </summary>
public string AmountFormatted { get; set; } = string.Empty;
/// <summary>
/// وضعیت (Pending/Calculated/Paid/Withdrawn)
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// رنگ Badge
/// </summary>
public string StatusBadgeColor { get; set; } = string.Empty;
/// <summary>
/// تاریخ محاسبه
/// </summary>
public DateTime CalculatedDate { get; set; }
/// <summary>
/// تاریخ شمسی
/// </summary>
public string DatePersian { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,27 @@
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
/// <summary>
/// دریافت تعادل هفتگی کاربر جاری
/// </summary>
public record GetMyWeeklyBalancesQuery : IRequest<GetMyWeeklyBalancesResponseDto>
{
/// <summary>
/// شماره هفته در فرمت ISO (مثال: "2025-W48"، null = هفته جاری)
/// </summary>
public string? WeekNumber { get; init; }
/// <summary>
/// فقط تعادل‌های فعال (غیر منقضی)
/// </summary>
public bool OnlyActive { get; init; } = true;
/// <summary>
/// شماره صفحه (1-based)
/// </summary>
public int PageNumber { get; init; } = 1;
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; init; } = 10;
}

View File

@@ -0,0 +1,66 @@
using CMSMicroservice.Protobuf.Protos.Commission;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetMyWeeklyBalancesQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetMyWeeklyBalancesResponseDto> Handle(GetMyWeeklyBalancesQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
// Fixed: Use proto-aligned request
var cmsRequest = new GetUserWeeklyBalancesRequest
{
UserId = userId,
OnlyActive = request.OnlyActive,
PageIndex = Math.Max(0, request.PageNumber - 1), // Convert 1-based to 0-based
PageSize = request.PageSize
};
// WeekNumber is string in proto (format: "YYYY-Www")
if (!string.IsNullOrEmpty(request.WeekNumber))
cmsRequest.WeekNumber = request.WeekNumber;
var response = await _context.Commission.GetUserWeeklyBalancesAsync(cmsRequest, cancellationToken: cancellationToken);
// Map list of UserWeeklyBalanceModel to DTO
var balances = response.Models.Select(b => new WeeklyBalanceItemDto
{
Id = b.Id,
WeekNumber = b.WeekNumber,
LeftLegBalances = b.LeftLegBalances,
RightLegBalances = b.RightLegBalances,
TotalBalances = b.TotalBalances,
WeeklyPoolContribution = b.WeeklyPoolContribution,
CalculatedAt = b.CalculatedAt?.ToDateTime(),
IsExpired = b.IsExpired
}).ToList();
// Calculate summary
var totalLeft = balances.Sum(b => b.LeftLegBalances);
var totalRight = balances.Sum(b => b.RightLegBalances);
var weakerLeg = totalLeft < totalRight ? "Left" : "Right";
return new GetMyWeeklyBalancesResponseDto
{
Balances = balances,
TotalCount = (int)response.MetaData.TotalCount,
PageNumber = request.PageNumber,
PageSize = request.PageSize,
TotalLeftBalances = totalLeft,
TotalRightBalances = totalRight,
WeakerLeg = weakerLeg
};
}
}

View File

@@ -0,0 +1,82 @@
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
public class GetMyWeeklyBalancesResponseDto
{
/// <summary>
/// لیست تعادل‌های هفتگی
/// </summary>
public List<WeeklyBalanceItemDto> Balances { get; set; } = new();
/// <summary>
/// تعداد کل رکوردها
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// شماره صفحه
/// </summary>
public int PageNumber { get; set; }
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// مجموع تعادل پای چپ
/// </summary>
public int TotalLeftBalances { get; set; }
/// <summary>
/// مجموع تعادل پای راست
/// </summary>
public int TotalRightBalances { get; set; }
/// <summary>
/// پای ضعیف‌تر (Left/Right)
/// </summary>
public string WeakerLeg { get; set; } = string.Empty;
}
public class WeeklyBalanceItemDto
{
/// <summary>
/// شناسه رکورد
/// </summary>
public long Id { get; set; }
/// <summary>
/// شماره هفته (فرمت: "2025-W48")
/// </summary>
public string WeekNumber { get; set; } = string.Empty;
/// <summary>
/// تعادل پای چپ
/// </summary>
public int LeftLegBalances { get; set; }
/// <summary>
/// تعادل پای راست
/// </summary>
public int RightLegBalances { get; set; }
/// <summary>
/// مجموع تعادل
/// </summary>
public int TotalBalances { get; set; }
/// <summary>
/// سهم از استخر هفتگی
/// </summary>
public long WeeklyPoolContribution { get; set; }
/// <summary>
/// تاریخ محاسبه
/// </summary>
public DateTime? CalculatedAt { get; set; }
/// <summary>
/// آیا منقضی شده؟
/// </summary>
public bool IsExpired { get; set; }
}

View File

@@ -5,4 +5,12 @@ public class ForbiddenAccessException : Exception
public ForbiddenAccessException() : base()
{
}
public ForbiddenAccessException(string message) : base(message)
{
}
public ForbiddenAccessException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -1,9 +1,9 @@
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.OtpToken;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.PruductCategory;
using CMSMicroservice.Protobuf.Protos.ProductCategory;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.ProductImages;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserAddress;
@@ -14,6 +14,8 @@ using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using PYMSMicroservice.Protobuf.Protos.Transaction;
namespace FrontOffice.BFF.Application.Common.Interfaces;
@@ -29,10 +31,10 @@ public interface IApplicationContractContext
#region CMS
PackageContract.PackageContractClient Package { get; }
ProductsContract.ProductsContractClient Product { get; }
ProductGallerysContract.ProductGallerysContractClient ProductGallerys { get; }
ProductGalleriesContract.ProductGalleriesContractClient ProductGalleries { get; }
ProductImagesContract.ProductImagesContractClient ProductImages { get; }
CategoryContract.CategoryContractClient Category { get; }
PruductCategoryContract.PruductCategoryContractClient ProductCategory { get; }
ProductCategoryContract.ProductCategoryContractClient ProductCategory { get; }
UserCartsContract.UserCartsContractClient UserCart { get; }
UserContract.UserContractClient User { get; }
UserContractContract.UserContractContractClient UserContract { get; }
@@ -43,6 +45,10 @@ public interface IApplicationContractContext
UserWalletChangeLogContract.UserWalletChangeLogContractClient UserWalletChangeLog { get; }
ConfigurationContract.ConfigurationContractClient Configuration { get; }
CommissionContract.CommissionContractClient Commission { get; }
// Network & Club System
ClubMembershipContract.ClubMembershipContractClient ClubMemberships { get; }
NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships { get; }
#endregion
#region PYMS

View File

@@ -15,6 +15,8 @@
<ProjectReference Include="..\FrontOffice.BFF.Domain\FrontOffice.BFF.Domain.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.UserOrder.Protobuf\FrontOffice.BFF.UserOrder.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Category.Protobuf\FrontOffice.BFF.Category.Protobuf.csproj" />
<!-- CMS Protobuf for Commission, ClubMembership, NetworkMembership, Configuration -->
<ProjectReference Include="..\..\..\CMS\src\CMSMicroservice.Protobuf\CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
/// <summary>
/// دریافت آمار شبکه کاربر جاری
/// </summary>
public record GetMyNetworkStatisticsQuery : IRequest<GetMyNetworkStatisticsResponseDto>;

View File

@@ -0,0 +1,65 @@
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
public class GetMyNetworkStatisticsQueryHandler : IRequestHandler<GetMyNetworkStatisticsQuery, GetMyNetworkStatisticsResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetMyNetworkStatisticsQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetMyNetworkStatisticsResponseDto> Handle(GetMyNetworkStatisticsQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
// Note: GetNetworkStatisticsRequest is empty (returns overall stats)
// For user-specific stats, we need to use GetUserNetwork instead
var cmsRequest = new GetNetworkStatisticsRequest();
var response = await _context.NetworkMemberships.GetNetworkStatisticsAsync(cmsRequest, cancellationToken: cancellationToken);
// Also get user's own network info for personal stats
var userNetworkRequest = new GetUserNetworkRequest { UserId = userId };
var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken: cancellationToken);
var weakerLeg = response.LeftLegCount < response.RightLegCount ? "Left" : "Right";
// Find last member from TopUsers if available
var lastMember = response.TopUsers.LastOrDefault();
return new GetMyNetworkStatisticsResponseDto
{
// Overall network stats
TotalMembers = response.TotalMembers,
ActiveMembers = response.ActiveMembers,
LeftLegCount = response.LeftLegCount,
RightLegCount = response.RightLegCount,
LeftPercentage = response.LeftPercentage,
RightPercentage = response.RightPercentage,
AverageDepth = response.AverageDepth,
MaxDepth = response.MaxDepth,
WeakerLeg = weakerLeg,
// User's personal info
MyNetworkLevel = userNetwork.NetworkLevel,
MyNetworkLeg = userNetwork.NetworkLeg == 0 ? "Left" : "Right",
MyReferralCode = userNetwork.ReferralCode,
// Last member info
LastMember = lastMember != null ? new LastMemberDto
{
UserId = lastMember.UserId,
FullName = lastMember.UserName,
Position = lastMember.LeftCount > lastMember.RightCount ? "Left" : "Right",
TotalChildren = lastMember.TotalChildren
} : null
};
}
}

View File

@@ -0,0 +1,77 @@
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
public class GetMyNetworkStatisticsResponseDto
{
/// <summary>
/// تعداد کل اعضا
/// </summary>
public int TotalMembers { get; set; }
/// <summary>
/// تعداد اعضای فعال
/// </summary>
public int ActiveMembers { get; set; }
/// <summary>
/// تعداد اعضای پای چپ
/// </summary>
public int LeftLegCount { get; set; }
/// <summary>
/// تعداد اعضای پای راست
/// </summary>
public int RightLegCount { get; set; }
/// <summary>
/// درصد پای چپ
/// </summary>
public double LeftPercentage { get; set; }
/// <summary>
/// درصد پای راست
/// </summary>
public double RightPercentage { get; set; }
/// <summary>
/// میانگین عمق درخت
/// </summary>
public double AverageDepth { get; set; }
/// <summary>
/// حداکثر عمق درخت
/// </summary>
public int MaxDepth { get; set; }
/// <summary>
/// پای ضعیف‌تر (Weaker Leg)
/// </summary>
public string WeakerLeg { get; set; } = string.Empty;
/// <summary>
/// سطح کاربر در شبکه
/// </summary>
public int MyNetworkLevel { get; set; }
/// <summary>
/// پای کاربر در شبکه (Left/Right)
/// </summary>
public string MyNetworkLeg { get; set; } = string.Empty;
/// <summary>
/// کد معرفی کاربر
/// </summary>
public string MyReferralCode { get; set; } = string.Empty;
/// <summary>
/// آخرین عضو اضافه شده
/// </summary>
public LastMemberDto? LastMember { get; set; }
}
public class LastMemberDto
{
public long UserId { get; set; }
public string FullName { get; set; } = string.Empty;
public string Position { get; set; } = string.Empty;
public int TotalChildren { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
/// <summary>
/// دریافت درخت شبکه باینری کاربر جاری
/// </summary>
public record GetMyNetworkTreeQuery : IRequest<GetMyNetworkTreeResponseDto>
{
/// <summary>
/// حداکثر عمق درخت (1-10)
/// </summary>
public int MaxDepth { get; init; } = 3;
}

View File

@@ -0,0 +1,94 @@
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
public class GetMyNetworkTreeQueryHandler : IRequestHandler<GetMyNetworkTreeQuery, GetMyNetworkTreeResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetMyNetworkTreeQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetMyNetworkTreeResponseDto> Handle(GetMyNetworkTreeQuery request, CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
var cmsRequest = new GetNetworkTreeRequest
{
RootUserId = userId,
MaxDepth = Math.Clamp(request.MaxDepth, 1, 10) // محدود کردن بین 1-10
};
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken: cancellationToken);
// CMS returns flat list (repeated NetworkTreeNodeModel nodes)
// We need to build tree structure from it
var rootNode = BuildTreeFromFlatList(response.Nodes.ToList(), userId);
return new GetMyNetworkTreeResponseDto
{
RootNode = rootNode,
TotalMembers = response.Nodes.Count,
CurrentDepth = CalculateDepth(rootNode)
};
}
/// <summary>
/// Build hierarchical tree from flat list returned by CMS
/// </summary>
private NetworkNodeDto? BuildTreeFromFlatList(List<NetworkTreeNodeModel> flatNodes, long rootUserId)
{
if (flatNodes == null || flatNodes.Count == 0)
return null;
// Create lookup dictionary for O(1) access
var nodeDict = flatNodes.ToDictionary(n => n.UserId);
// Find root node
var rootCmsNode = flatNodes.FirstOrDefault(n => n.UserId == rootUserId);
if (rootCmsNode == null)
return null;
// Build tree recursively
return BuildNodeRecursive(rootCmsNode, flatNodes, nodeDict, 0);
}
private NetworkNodeDto BuildNodeRecursive(
NetworkTreeNodeModel cmsNode,
List<NetworkTreeNodeModel> allNodes,
Dictionary<long, NetworkTreeNodeModel> nodeDict,
int level)
{
// Find children (nodes that have this node as parent)
var leftChild = allNodes.FirstOrDefault(n =>
n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 0); // 0 = Left
var rightChild = allNodes.FirstOrDefault(n =>
n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 1); // 1 = Right
var position = level == 0 ? "Root" : (cmsNode.NetworkLeg == 0 ? "Left" : "Right");
return new NetworkNodeDto
{
UserId = cmsNode.UserId,
FullName = cmsNode.UserName ?? string.Empty,
Mobile = string.Empty, // Proto doesn't have mobile, add if needed
Avatar = null, // Proto doesn't have avatar
Position = position,
Level = level,
LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null,
RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null
};
}
private int CalculateDepth(NetworkNodeDto? node)
{
if (node == null) return 0;
return 1 + Math.Max(CalculateDepth(node.LeftChild), CalculateDepth(node.RightChild));
}
}

View File

@@ -0,0 +1,67 @@
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
public class GetMyNetworkTreeResponseDto
{
/// <summary>
/// نود ریشه (کاربر جاری)
/// </summary>
public NetworkNodeDto? RootNode { get; set; }
/// <summary>
/// تعداد کل اعضا در درخت
/// </summary>
public int TotalMembers { get; set; }
/// <summary>
/// عمق فعلی درخت
/// </summary>
public int CurrentDepth { get; set; }
}
public class NetworkNodeDto
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// نام کاربر
/// </summary>
public string FullName { get; set; } = string.Empty;
/// <summary>
/// موبایل
/// </summary>
public string Mobile { get; set; } = string.Empty;
/// <summary>
/// آواتار
/// </summary>
public string? Avatar { get; set; }
/// <summary>
/// موقعیت در درخت (Root/Left/Right)
/// </summary>
public string Position { get; set; } = string.Empty;
/// <summary>
/// فرزند چپ
/// </summary>
public NetworkNodeDto? LeftChild { get; set; }
/// <summary>
/// فرزند راست
/// </summary>
public NetworkNodeDto? RightChild { get; set; }
/// <summary>
/// سطح در درخت (0 = root)
/// </summary>
public int Level { get; set; }
/// <summary>
/// آیا فرزند دارد؟
/// </summary>
public bool HasChildren => LeftChild != null || RightChild != null;
}

View File

@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using CMSCategory = CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.ProductImages;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSPruductCategory = CMSMicroservice.Protobuf.Protos.PruductCategory;
using CMSProductCategory = CMSMicroservice.Protobuf.Protos.ProductCategory;
namespace FrontOffice.BFF.Application.ProductsCQ.Queries.GetProducts;
public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProductsResponseDto>
@@ -28,10 +28,10 @@ public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProd
var dto = response.Adapt<GetProductsResponseDto>();
var galleryResponse = await _context.ProductGallerys.GetAllProductGallerysByFilterAsync(
new GetAllProductGallerysByFilterRequest
var galleryResponse = await _context.ProductGalleries.GetAllProductGalleriesByFilterAsync(
new GetAllProductGalleriesByFilterRequest
{
Filter = new GetAllProductGallerysByFilterFilter()
Filter = new GetAllProductGalleriesByFilterFilter()
}, cancellationToken: cancellationToken);
var relatedItems = galleryResponse?.Models?.Where(x => x.ProductId == request.Id).ToList();
@@ -82,10 +82,10 @@ public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProd
var categoryLookup = categoriesResponse.Models?
.ToDictionary(c => c.Id) ?? new Dictionary<long, CMSCategory.GetAllCategoryByFilterResponseModel>();
var productCategoryResponse = await _context.ProductCategory.GetAllPruductCategoryByFilterAsync(
new CMSPruductCategory.GetAllPruductCategoryByFilterRequest
var productCategoryResponse = await _context.ProductCategory.GetAllProductCategoryByFilterAsync(
new CMSProductCategory.GetAllProductCategoryByFilterRequest
{
Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter
Filter = new CMSProductCategory.GetAllProductCategoryByFilterFilter
{
ProductId = productId
},

View File

@@ -1,16 +1,32 @@
using CMSMicroservice.Protobuf.Protos.User;
namespace FrontOffice.BFF.Application.UserCQ.Commands.DeleteUser;
public class DeleteUserCommandHandler : IRequestHandler<DeleteUserCommand, Unit>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public DeleteUserCommandHandler(IApplicationContractContext context)
public DeleteUserCommandHandler(IApplicationContractContext context, ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<Unit> Handle(DeleteUserCommand request, CancellationToken cancellationToken)
{
//TODO: Implement your business logic
return new Unit();
// Security check: User can only delete their own account
var currentUserId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
if (request.Id != currentUserId)
{
throw new ForbiddenAccessException("You can only delete your own account");
}
await _context.User.DeleteUserAsync(new DeleteUserRequest
{
Id = request.Id
}, cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@@ -1,16 +1,29 @@
namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.TransferUserWalletBallance;
/// <summary>
/// انتقال موجودی کیف پول - نیاز به پیاده‌سازی در CMS
/// TODO: این قابلیت نیاز به افزودن rpc TransferBalance در userwallet.proto دارد
/// </summary>
public class TransferUserWalletBallanceCommandHandler : IRequestHandler<TransferUserWalletBallanceCommand, Unit>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public TransferUserWalletBallanceCommandHandler(IApplicationContractContext context)
public TransferUserWalletBallanceCommandHandler(IApplicationContractContext context, ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<Unit> Handle(TransferUserWalletBallanceCommand request, CancellationToken cancellationToken)
{
//TODO: Implement your business logic
return new Unit();
// TODO: این قابلیت نیاز به پیاده‌سازی در CMS دارد
// 1. افزودن rpc TransferBalance در userwallet.proto
// 2. پیاده‌سازی logic در CMS service
// 3. تکمیل این handler
throw new NotImplementedException(
"Transfer Balance functionality is not yet implemented in CMS. " +
"Please add TransferBalance RPC to userwallet.proto first.");
}
}

View File

@@ -19,17 +19,23 @@ public class
public async Task<GetAllUserWalletChangeLogResponseDto> Handle(GetAllUserWalletChangeLogQuery request,
CancellationToken cancellationToken)
{
var filter = new GetAllUserWalletChangeLogByFilterFilter();
if (_currentUserService.UserId.HasValue)
filter.UserId = _currentUserService.UserId.Value;
if (request.ReferenceId.HasValue)
filter.RefrenceId = request.ReferenceId.Value;
if (request.IsIncrease.HasValue)
filter.IsIncrease = request.IsIncrease.Value;
var result = await _context.UserWalletChangeLog.GetAllUserWalletChangeLogByFilterAsync(
new GetAllUserWalletChangeLogByFilterRequest()
{
Filter = new GetAllUserWalletChangeLogByFilterFilter()
{
UserId = _currentUserService.UserId.Value,
RefrenceId = request.ReferenceId.HasValue ? new Google.Protobuf.WellKnownTypes.Int64Value { Value = request.ReferenceId.Value } : null,
IsIncrease = request.IsIncrease.HasValue ? new Google.Protobuf.WellKnownTypes.BoolValue { Value = request.IsIncrease.Value } : null
}
Filter = filter
}, cancellationToken: cancellationToken);
var finalResult= result.Adapt<GetAllUserWalletChangeLogResponseDto>();
var finalResult = result.Adapt<GetAllUserWalletChangeLogResponseDto>();
return finalResult;
}
}

View File

@@ -17,8 +17,8 @@ public class GetUserWithdrawalsQueryHandler : IRequestHandler<GetUserWithdrawals
{
var response = await _context.Commission.GetUserCommissionPayoutsAsync(new GetUserCommissionPayoutsRequest()
{
UserId = _currentUserService.UserId,
Status = request.Status.HasValue ? new Google.Protobuf.WellKnownTypes.Int32Value { Value = request.Status.Value } : null,
UserId = _currentUserService.UserId, // long? type - assign directly
Status = request.Status, // int? type - assign directly
PageIndex = 1,
PageSize = 50
}, cancellationToken: cancellationToken);
@@ -33,7 +33,7 @@ public class GetUserWithdrawalsQueryHandler : IRequestHandler<GetUserWithdrawals
WeekNumber = m.WeekNumber,
TotalAmount = m.TotalAmount,
Status = m.Status,
WithdrawalMethod = m.WithdrawalMethod?.Value,
WithdrawalMethod = m.WithdrawalMethod, // int? type - no .Value needed
IbanNumber = m.IbanNumber,
Created = m.Created.ToDateTime()
}).ToList()

View File

@@ -1,9 +1,9 @@
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.OtpToken;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.PruductCategory;
using CMSMicroservice.Protobuf.Protos.ProductCategory;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.ProductImages;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserAddress;
@@ -14,6 +14,8 @@ using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using FrontOffice.BFF.Application.Common.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using PYMSMicroservice.Protobuf.Protos.Transaction;
@@ -53,10 +55,10 @@ public class ApplicationContractContext : IApplicationContractContext
#region CMS
public PackageContract.PackageContractClient Package => GetService<PackageContract.PackageContractClient>();
public ProductsContract.ProductsContractClient Product => GetService<ProductsContract.ProductsContractClient>();
public ProductGallerysContract.ProductGallerysContractClient ProductGallerys => GetService<ProductGallerysContract.ProductGallerysContractClient>();
public ProductGalleriesContract.ProductGalleriesContractClient ProductGalleries => GetService<ProductGalleriesContract.ProductGalleriesContractClient>();
public ProductImagesContract.ProductImagesContractClient ProductImages => GetService<ProductImagesContract.ProductImagesContractClient>();
public CategoryContract.CategoryContractClient Category => GetService<CategoryContract.CategoryContractClient>();
public PruductCategoryContract.PruductCategoryContractClient ProductCategory => GetService<PruductCategoryContract.PruductCategoryContractClient>();
public ProductCategoryContract.ProductCategoryContractClient ProductCategory => GetService<ProductCategoryContract.ProductCategoryContractClient>();
public UserCartsContract.UserCartsContractClient UserCart => GetService<UserCartsContract.UserCartsContractClient>();
public UserContract.UserContractClient User => GetService<UserContract.UserContractClient>();
@@ -70,6 +72,10 @@ public class ApplicationContractContext : IApplicationContractContext
public UserWalletChangeLogContract.UserWalletChangeLogContractClient UserWalletChangeLog => GetService<UserWalletChangeLogContract.UserWalletChangeLogContractClient>();
public ConfigurationContract.ConfigurationContractClient Configuration => GetService<ConfigurationContract.ConfigurationContractClient>();
public CommissionContract.CommissionContractClient Commission => GetService<CommissionContract.CommissionContractClient>();
// Network & Club System
public ClubMembershipContract.ClubMembershipContractClient ClubMemberships => GetService<ClubMembershipContract.ClubMembershipContractClient>();
public NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships => GetService<NetworkMembershipContract.NetworkMembershipContractClient>();
#endregion
#region PYMS

View File

@@ -0,0 +1,38 @@
using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership;
namespace FrontOffice.BFF.WebApi.Services;
/// <summary>
/// سرویس باشگاه مشتریان - مختص کاربر جاری
/// این سرویس فعلاً از REST API استفاده می‌کند تا proto فایل برایش ایجاد شود
/// TODO: ایجاد proto فایل و تبدیل به gRPC service
/// </summary>
public class ClubMembershipService
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public ClubMembershipService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
/// <summary>
/// دریافت وضعیت عضویت باشگاه کاربر جاری
/// </summary>
public async Task<GetMyClubMembershipResponseDto> GetMyClubMembership(Empty request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetMyClubMembershipQuery, GetMyClubMembershipResponseDto>(context);
}
/// <summary>
/// فعال‌سازی عضویت باشگاه (پرداخت 56M)
/// </summary>
public async Task<ActivateMyClubMembershipResponseDto> ActivateMyClubMembership(
ActivateMyClubMembershipCommand request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ActivateMyClubMembershipCommand, ActivateMyClubMembershipResponseDto>(request, context);
}
}