feat: Implement customer profile and referral queries

- Add GetCustomerProfileResponseDto for retrieving customer profile information.
- Create GetCustomerReferralsQuery and GetCustomerReferralsQueryHandler to fetch customer referrals with pagination and filtering options.
- Introduce GetCustomerReferralsResponseDto to structure the response for customer referrals.
- Implement GetCustomerSettingsQuery and GetCustomerSettingsQueryHandler to retrieve user settings.
- Add GetCustomerOrder and GetCustomerOrderQueryHandler for fetching specific customer orders.
- Create GetCustomerOrderHistoryQuery and GetCustomerOrderHistoryQueryHandler to retrieve order history with filtering options.
- Implement GetCustomerOrdersQuery and GetCustomerOrdersQueryHandler for fetching multiple customer orders with filters.
- Add GetCustomerWalletChangeLogQuery and GetCustomerWalletChangeLogQueryHandler for retrieving wallet change logs.
- Implement GetCustomerWithdrawalSettingsQuery and GetCustomerWithdrawalSettingsQueryHandler for fetching withdrawal settings.
- Create GetCustomerWithdrawalsQuery and GetCustomerWithdrawalsQueryHandler to retrieve customer withdrawal requests.
This commit is contained in:
masoodafar-web
2026-02-05 23:01:50 +03:30
parent b41342dcad
commit b2d676b555
96 changed files with 4822 additions and 589 deletions
@@ -0,0 +1,15 @@
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
/// <summary>
/// Query برای دریافت درخت شبکه کاربر جاری (Customer-facing)
/// از ICurrentUserService برای دریافت UserId استفاده می‌کند
/// </summary>
public record GetMyNetworkTreeQuery : IRequest<NetworkTreeDto?>
{
/// <summary>
/// تعداد سطوح (Depth) که می‌خواهیم نمایش دهیم (پیش‌فرض: 3)
/// </summary>
public int MaxDepth { get; init; } = 3;
}
@@ -0,0 +1,40 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
/// <summary>
/// Handler برای دریافت درخت شبکه کاربر جاری
/// </summary>
public class GetMyNetworkTreeQueryHandler : IRequestHandler<GetMyNetworkTreeQuery, NetworkTreeDto?>
{
private readonly ICurrentUserService _currentUser;
private readonly ISender _sender;
public GetMyNetworkTreeQueryHandler(
ICurrentUserService currentUser,
ISender sender)
{
_currentUser = currentUser;
_sender = sender;
}
public async Task<NetworkTreeDto?> Handle(GetMyNetworkTreeQuery request, CancellationToken cancellationToken)
{
// دریافت UserId از JWT
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
if (userId == 0)
{
throw new UnauthorizedAccessException("User not authenticated");
}
// استفاده از GetNetworkTreeQuery موجود با UserId از JWT
var query = new GetNetworkTreeQuery
{
UserId = userId,
MaxDepth = request.MaxDepth > 0 ? request.MaxDepth : 3
};
return await _sender.Send(query, cancellationToken);
}
}
@@ -0,0 +1,24 @@
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
public class GetMyNetworkTreeQueryValidator : AbstractValidator<GetMyNetworkTreeQuery>
{
public GetMyNetworkTreeQueryValidator()
{
RuleFor(x => x.MaxDepth)
.InclusiveBetween(1, 100)
.WithMessage("عمق درخت باید بین 1 تا 100 باشد");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetMyNetworkTreeQuery>.CreateWithOptions(
(GetMyNetworkTreeQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -2,5 +2,8 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
public class GetNetworkStatisticsQuery : IRequest<GetNetworkStatisticsResponseDto>
{
// No parameters - returns overall statistics
/// <summary>
/// شناسه کاربر برای محاسبه آمار شبکه او - 0 یا null یعنی کاربر جاری
/// </summary>
public long UserId { get; set; }
}
@@ -5,61 +5,79 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatisticsQuery, GetNetworkStatisticsResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public GetNetworkStatisticsQueryHandler(IApplicationDbContext context)
public GetNetworkStatisticsQueryHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<GetNetworkStatisticsResponseDto> Handle(GetNetworkStatisticsQuery request, CancellationToken cancellationToken)
{
// Basic statistics - using Users table with NetworkParentId
var totalMembers = await _context.Users
.Where(x => x.NetworkParentId != null)
.CountAsync(cancellationToken);
var activeMembers = await _context.Users
.Where(x => x.NetworkParentId != null)
.CountAsync(cancellationToken);
// Get userId - use current user if not specified or is 0
var userId = request.UserId == 0
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
: request.UserId;
var leftLegCount = await _context.Users
.Where(x => x.LegPosition == NetworkLeg.Left)
.CountAsync(cancellationToken);
if (userId == 0)
{
throw new UnauthorizedAccessException("User ID not found");
}
var rightLegCount = await _context.Users
.Where(x => x.LegPosition == NetworkLeg.Right)
.CountAsync(cancellationToken);
// Get all descendants recursively
var allUsers = await _context.Users.ToListAsync(cancellationToken);
var allDescendants = GetAllDescendants(userId, allUsers);
// Statistics for the user's network (all descendants)
var totalMembers = allDescendants.Count;
var activeMembers = allDescendants.Count(x => !x.IsDeleted);
// Get direct left and right children
var leftChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left);
var rightChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right);
// Count all descendants in left and right subtrees
var leftLegCount = leftChild != null ? GetAllDescendants(leftChild.Id, allUsers).Count + 1 : 0; // +1 for leftChild itself
var rightLegCount = rightChild != null ? GetAllDescendants(rightChild.Id, allUsers).Count + 1 : 0; // +1 for rightChild itself
double leftPercentage = totalMembers > 0 ? (leftLegCount / (double)totalMembers) * 100 : 0;
double rightPercentage = totalMembers > 0 ? (rightLegCount / (double)totalMembers) * 100 : 0;
// Calculate depth based on network parent relationships
// For simplicity, we'll estimate average depth as 3-5 levels
double averageDepth = 4.5; // Estimated average
int maxDepth = 10; // Estimated max depth
// Level distribution - simplified estimation based on growth pattern
var levelDistribution = new List<LevelDistributionModel>();
if (totalMembers > 0)
// Calculate actual depth
int maxDepth = 0;
double totalDepthSum = 0;
var userDepths = new Dictionary<long, int>();
CalculateDepths(userId, allUsers, 0, userDepths, ref maxDepth);
if (allDescendants.Count > 0)
{
// Approximate distribution: Level 1 (10%), Level 2 (20%), Level 3 (30%), Level 4 (20%), Level 5+ (20%)
levelDistribution = new List<LevelDistributionModel>
{
new() { Level = 1, Count = (int)(totalMembers * 0.1) },
new() { Level = 2, Count = (int)(totalMembers * 0.2) },
new() { Level = 3, Count = (int)(totalMembers * 0.3) },
new() { Level = 4, Count = (int)(totalMembers * 0.2) },
new() { Level = 5, Count = (int)(totalMembers * 0.15) },
new() { Level = 6, Count = totalMembers - (int)(totalMembers * 0.95) }
};
totalDepthSum = allDescendants.Sum(d => userDepths.ContainsKey(d.Id) ? userDepths[d.Id] : 0);
}
double averageDepth = allDescendants.Count > 0 ? totalDepthSum / allDescendants.Count : 0;
// Level distribution - calculate from depths
var levelDistribution = new List<LevelDistributionModel>();
if (allDescendants.Count > 0)
{
var levelCounts = allDescendants
.Where(d => userDepths.ContainsKey(d.Id))
.GroupBy(d => userDepths[d.Id])
.OrderBy(g => g.Key)
.Select(g => new LevelDistributionModel { Level = g.Key, Count = g.Count() })
.ToList();
levelDistribution = levelCounts;
}
// Monthly growth (last 6 months) - using Created date
// Monthly growth (last 6 months) - using descendants Created date
var sixMonthsAgo = DateTime.Now.AddMonths(-6);
var monthlyGrowthRaw = await _context.Users
.Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo)
var monthlyGrowthRaw = allDescendants
.Where(x => x.Created >= sixMonthsAgo)
.Select(x => new { x.Created.Year, x.Created.Month })
.ToListAsync(cancellationToken);
.ToList();
var monthlyGrowth = monthlyGrowthRaw
.GroupBy(x => new { x.Year, x.Month })
@@ -71,27 +89,34 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
.OrderBy(x => x.Month)
.ToList();
// Top users by total children count
var topUsers = await _context.Users
.Where(x => x.NetworkParentId != null)
// Top users by total descendants count
var userDescendantCounts = new Dictionary<long, int>();
foreach (var user in allDescendants)
{
var descendants = GetAllDescendants(user.Id, allUsers);
userDescendantCounts[user.Id] = descendants.Count;
}
var topUserData = allDescendants
.Where(x => x.Id != userId && userDescendantCounts[x.Id] > 0)
.Select(x => new
{
x.Id,
UserName = (x.FirstName + " " + x.LastName).Trim(),
LeftCount = _context.Users.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Left),
RightCount = _context.Users.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Right)
DescendantCount = userDescendantCounts[x.Id],
LeftCount = allUsers.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Left),
RightCount = allUsers.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Right)
})
.Where(x => x.LeftCount + x.RightCount > 0)
.OrderByDescending(x => x.LeftCount + x.RightCount)
.OrderByDescending(x => x.DescendantCount)
.Take(10)
.ToListAsync(cancellationToken);
.ToList();
var topUserModels = topUsers.Select((x, index) => new TopNetworkUserModel
var topUserModels = topUserData.Select((x, index) => new TopNetworkUserModel
{
Rank = index + 1,
UserId = x.Id,
UserName = x.UserName,
TotalChildren = x.LeftCount + x.RightCount,
TotalChildren = x.DescendantCount,
LeftCount = x.LeftCount,
RightCount = x.RightCount
}).ToList();
@@ -111,4 +136,40 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
TopUsers = topUserModels
};
}
/// <summary>
/// Recursively get all descendants of a user
/// </summary>
private List<User> GetAllDescendants(long userId, List<User> allUsers)
{
var descendants = new List<User>();
var directChildren = allUsers.Where(x => x.NetworkParentId == userId).ToList();
foreach (var child in directChildren)
{
descendants.Add(child);
descendants.AddRange(GetAllDescendants(child.Id, allUsers));
}
return descendants;
}
/// <summary>
/// Calculate depth for all descendants recursively
/// </summary>
private void CalculateDepths(long userId, List<User> allUsers, int currentDepth, Dictionary<long, int> depths, ref int maxDepth)
{
var children = allUsers.Where(x => x.NetworkParentId == userId).ToList();
foreach (var child in children)
{
var childDepth = currentDepth + 1;
depths[child.Id] = childDepth;
if (childDepth > maxDepth)
maxDepth = childDepth;
CalculateDepths(child.Id, allUsers, childDepth, depths, ref maxDepth);
}
}
}
@@ -8,21 +8,37 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
{
private readonly IApplicationDbContext _context;
private readonly ILogger<GetNetworkTreeQueryHandler> _logger;
private readonly ICurrentUserService _currentUser;
public GetNetworkTreeQueryHandler(
IApplicationDbContext context,
ILogger<GetNetworkTreeQueryHandler> logger)
ILogger<GetNetworkTreeQueryHandler> logger,
ICurrentUserService currentUser)
{
_context = context;
_logger = logger;
_currentUser = currentUser;
}
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
{
// Get userId - use current user if UserId is 0
var userId = request.UserId == 0
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
: request.UserId;
if (userId == 0)
{
throw new UnauthorizedAccessException("User ID not found");
}
// Create a new request with the resolved userId
var resolvedRequest = request with { UserId = userId };
try
{
// دریافت نتایج flat از Stored Procedure
var flatNodes = await ExecuteStoredProcedureAsync(request, cancellationToken);
var flatNodes = await ExecuteStoredProcedureAsync(resolvedRequest, cancellationToken);
if (flatNodes == null || !flatNodes.Any())
{