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:
@@ -0,0 +1,6 @@
|
||||
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
|
||||
|
||||
/// <summary>
|
||||
/// دریافت آمار شبکه کاربر جاری
|
||||
/// </summary>
|
||||
public record GetMyNetworkStatisticsQuery : IRequest<GetMyNetworkStatisticsResponseDto>;
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user