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,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;
}