feat: Implement Approve and Reject Withdrawal commands with handlers

This commit is contained in:
masoodafar-web
2025-12-01 16:48:07 +03:30
parent 8d31a8c026
commit 4aaf2247ff
27 changed files with 989 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
public class GetClubStatisticsQuery : IRequest<GetClubStatisticsResponseDto>
{
// No parameters - returns overall statistics
}

View File

@@ -0,0 +1,95 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
public class GetClubStatisticsQueryHandler : IRequestHandler<GetClubStatisticsQuery, GetClubStatisticsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetClubStatisticsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetClubStatisticsResponseDto> Handle(GetClubStatisticsQuery request, CancellationToken cancellationToken)
{
var now = DateTime.UtcNow;
// Basic statistics
var totalMembers = await _context.ClubMemberships.CountAsync(cancellationToken);
var activeMembers = await _context.ClubMemberships
.Where(x => x.IsActive)
.CountAsync(cancellationToken);
var inactiveMembers = totalMembers - activeMembers;
var expiredMembers = 0; // Since there's no expiration tracking in the model
double activePercentage = totalMembers > 0 ? (activeMembers / (double)totalMembers) * 100 : 0;
// Package distribution - ClubMembership doesn't have PackageId
// We'll return empty list for now or create mock data
var packageDistribution = new List<PackageLevelDistributionModel>();
// Monthly trend (last 6 months)
var sixMonthsAgo = now.AddMonths(-6);
var activations = await _context.ClubMemberships
.Where(x => x.ActivatedAt >= sixMonthsAgo && x.ActivatedAt != null)
.GroupBy(x => new { x.ActivatedAt!.Value.Year, x.ActivatedAt.Value.Month })
.Select(g => new { g.Key.Year, g.Key.Month, Count = g.Count() })
.ToListAsync(cancellationToken);
var monthlyTrend = new List<MonthlyMembershipTrendModel>();
for (int i = 5; i >= 0; i--)
{
var targetDate = now.AddMonths(-i);
var year = targetDate.Year;
var month = targetDate.Month;
var activationCount = activations.FirstOrDefault(x => x.Year == year && x.Month == month)?.Count ?? 0;
monthlyTrend.Add(new MonthlyMembershipTrendModel
{
Month = $"{year}-{month:D2}",
Activations = activationCount,
Expirations = 0, // No expiration tracking
NetChange = activationCount
});
}
// Total revenue - sum of initial contributions
var totalRevenue = await _context.ClubMemberships
.SumAsync(x => x.InitialContribution, cancellationToken);
// Average membership duration - calculate from ActivatedAt to now
var activeMemberships = await _context.ClubMemberships
.Where(x => x.IsActive && x.ActivatedAt != null)
.Select(x => x.ActivatedAt!.Value)
.ToListAsync(cancellationToken);
double averageDuration = 0;
if (activeMemberships.Any())
{
var durations = activeMemberships
.Select(activatedAt => (now - activatedAt).TotalDays)
.ToList();
averageDuration = durations.Average();
}
// Expiring soon count - not applicable since no expiration tracking
int expiringSoonCount = 0;
return new GetClubStatisticsResponseDto
{
TotalMembers = totalMembers,
ActiveMembers = activeMembers,
InactiveMembers = inactiveMembers,
ExpiredMembers = expiredMembers,
ActivePercentage = activePercentage,
PackageDistribution = packageDistribution,
MonthlyTrend = monthlyTrend,
TotalRevenue = totalRevenue,
AverageMembershipDurationDays = averageDuration,
ExpiringSoonCount = expiringSoonCount
};
}
}

View File

@@ -0,0 +1,31 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
public class GetClubStatisticsResponseDto
{
public int TotalMembers { get; set; }
public int ActiveMembers { get; set; }
public int InactiveMembers { get; set; }
public int ExpiredMembers { get; set; }
public double ActivePercentage { get; set; }
public List<PackageLevelDistributionModel> PackageDistribution { get; set; } = new();
public List<MonthlyMembershipTrendModel> MonthlyTrend { get; set; } = new();
public long TotalRevenue { get; set; }
public double AverageMembershipDurationDays { get; set; }
public int ExpiringSoonCount { get; set; }
}
public class PackageLevelDistributionModel
{
public long PackageId { get; set; }
public string PackageName { get; set; } = string.Empty;
public int MemberCount { get; set; }
public double Percentage { get; set; }
}
public class MonthlyMembershipTrendModel
{
public string Month { get; set; } = string.Empty;
public int Activations { get; set; }
public int Expirations { get; set; }
public int NetChange { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ApproveWithdrawal;
public class ApproveWithdrawalCommand : IRequest<Unit>
{
public long PayoutId { get; set; }
public string? Notes { get; set; }
}

View File

@@ -0,0 +1,55 @@
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Application.Common.Exceptions;
namespace CMSMicroservice.Application.CommissionCQ.Commands.ApproveWithdrawal;
public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
public ApproveWithdrawalCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(ApproveWithdrawalCommand request, CancellationToken cancellationToken)
{
var payout = await _context.UserCommissionPayouts
.FirstOrDefaultAsync(x => x.Id == request.PayoutId, cancellationToken);
if (payout == null)
{
throw new NotFoundException($"Payout با شناسه {request.PayoutId} یافت نشد");
}
if (payout.Status != CommissionPayoutStatus.WithdrawRequested)
{
throw new BadRequestException($"فقط درخواست‌های در وضعیت WithdrawRequested قابل تایید هستند");
}
// Update status to Withdrawn (approved)
payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = DateTime.UtcNow;
payout.LastModified = DateTime.UtcNow;
// TODO: Add PayoutHistory record
// var history = new CommissionPayoutHistory
// {
// PayoutId = payout.Id,
// UserId = payout.UserId,
// WeekNumber = payout.WeekNumber,
// AmountBefore = payout.TotalAmount,
// AmountAfter = payout.TotalAmount,
// OldStatus = (int)CommissionPayoutStatus.Pending,
// NewStatus = (int)CommissionPayoutStatus.Approved,
// Action = (int)CommissionPayoutAction.Approved,
// PerformedBy = "Admin", // TODO: Get from authenticated user
// Reason = request.Notes,
// Created = DateTime.UtcNow
// };
// _context.CommissionPayoutHistories.Add(history);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.RejectWithdrawal;
public class RejectWithdrawalCommand : IRequest<Unit>
{
public long PayoutId { get; set; }
public string Reason { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,54 @@
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Application.Common.Exceptions;
namespace CMSMicroservice.Application.CommissionCQ.Commands.RejectWithdrawal;
public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
public RejectWithdrawalCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(RejectWithdrawalCommand request, CancellationToken cancellationToken)
{
var payout = await _context.UserCommissionPayouts
.FirstOrDefaultAsync(x => x.Id == request.PayoutId, cancellationToken);
if (payout == null)
{
throw new NotFoundException($"Payout با شناسه {request.PayoutId} یافت نشد");
}
if (payout.Status != CommissionPayoutStatus.WithdrawRequested)
{
throw new BadRequestException($"فقط درخواست‌های در وضعیت WithdrawRequested قابل رد هستند");
}
// Update status to Cancelled (rejected)
payout.Status = CommissionPayoutStatus.Cancelled;
payout.LastModified = DateTime.UtcNow;
// TODO: Add PayoutHistory record with rejection reason
// var history = new CommissionPayoutHistory
// {
// PayoutId = payout.Id,
// UserId = payout.UserId,
// WeekNumber = payout.WeekNumber,
// AmountBefore = payout.TotalAmount,
// AmountAfter = payout.TotalAmount,
// OldStatus = (int)CommissionPayoutStatus.Pending,
// NewStatus = (int)CommissionPayoutStatus.Rejected,
// Action = (int)CommissionPayoutAction.Rejected,
// PerformedBy = "Admin", // TODO: Get from authenticated user
// Reason = request.Reason,
// Created = DateTime.UtcNow
// };
// _context.CommissionPayoutHistories.Add(history);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,29 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
public record TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculationResponseDto>
{
/// <summary>
/// شماره هفته (فرمت: "YYYY-Www")
/// </summary>
public string WeekNumber { get; init; } = string.Empty;
/// <summary>
/// اگر true باشد، محاسبات قبلی را حذف و دوباره محاسبه می‌کند
/// </summary>
public bool ForceRecalculate { get; init; }
/// <summary>
/// Skip balance calculation
/// </summary>
public bool SkipBalances { get; init; }
/// <summary>
/// Skip pool calculation
/// </summary>
public bool SkipPool { get; init; }
/// <summary>
/// Skip payout processing
/// </summary>
public bool SkipPayouts { get; init; }
}

View File

@@ -0,0 +1,95 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWeeklyCalculationCommand, TriggerWeeklyCalculationResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IMediator _mediator;
public TriggerWeeklyCalculationCommandHandler(
IApplicationDbContext context,
IMediator mediator)
{
_context = context;
_mediator = mediator;
}
public async Task<TriggerWeeklyCalculationResponseDto> Handle(
TriggerWeeklyCalculationCommand request,
CancellationToken cancellationToken)
{
var executionId = Guid.NewGuid().ToString();
var startedAt = DateTime.UtcNow;
try
{
// Validate week number format
if (string.IsNullOrWhiteSpace(request.WeekNumber))
{
return new TriggerWeeklyCalculationResponseDto
{
Success = false,
Message = "شماره هفته نمی‌تواند خالی باشد",
ExecutionId = executionId,
StartedAt = startedAt
};
}
var steps = new List<string>();
// Step 1: Calculate Weekly Balances
if (!request.SkipBalances)
{
await _mediator.Send(new CalculateWeeklyBalancesCommand
{
WeekNumber = request.WeekNumber,
ForceRecalculate = request.ForceRecalculate
}, cancellationToken);
steps.Add("محاسبه امتیازات هفتگی");
}
// Step 2: Calculate Weekly Commission Pool
if (!request.SkipPool)
{
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
{
WeekNumber = request.WeekNumber
}, cancellationToken);
steps.Add("محاسبه استخر کمیسیون");
}
// Step 3: Process User Payouts
if (!request.SkipPayouts)
{
await _mediator.Send(new ProcessUserPayoutsCommand
{
WeekNumber = request.WeekNumber,
ForceReprocess = request.ForceRecalculate
}, cancellationToken);
steps.Add("پردازش پرداخت‌های کاربران");
}
return new TriggerWeeklyCalculationResponseDto
{
Success = true,
Message = $"محاسبات هفته {request.WeekNumber} با موفقیت انجام شد. مراحل: {string.Join(", ", steps)}",
ExecutionId = executionId,
StartedAt = startedAt
};
}
catch (Exception ex)
{
return new TriggerWeeklyCalculationResponseDto
{
Success = false,
Message = $"خطا در اجرای محاسبات: {ex.Message}",
ExecutionId = executionId,
StartedAt = startedAt
};
}
}
}

View File

@@ -0,0 +1,9 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
public class TriggerWeeklyCalculationResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public string ExecutionId { get; set; } = string.Empty;
public DateTime StartedAt { get; set; }
}

View File

@@ -0,0 +1,32 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
/// <summary>
/// Query برای دریافت لیست تمام استخرهای کمیسیون هفتگی
/// </summary>
public record GetAllWeeklyPoolsQuery : IRequest<GetAllWeeklyPoolsResponseDto>
{
/// <summary>
/// از هفته (فیلتر اختیاری)
/// </summary>
public string? FromWeek { get; init; }
/// <summary>
/// تا هفته (فیلتر اختیاری)
/// </summary>
public string? ToWeek { get; init; }
/// <summary>
/// فقط Pool های محاسبه شده
/// </summary>
public bool? OnlyCalculated { get; init; }
/// <summary>
/// شماره صفحه
/// </summary>
public int PageIndex { get; init; } = 1;
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; init; } = 10;
}

View File

@@ -0,0 +1,67 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQuery, GetAllWeeklyPoolsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAllWeeklyPoolsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAllWeeklyPoolsResponseDto> Handle(GetAllWeeklyPoolsQuery request, CancellationToken cancellationToken)
{
var query = _context.WeeklyCommissionPools.AsNoTracking();
// Apply filters
if (!string.IsNullOrWhiteSpace(request.FromWeek))
{
query = query.Where(x => string.Compare(x.WeekNumber, request.FromWeek) >= 0);
}
if (!string.IsNullOrWhiteSpace(request.ToWeek))
{
query = query.Where(x => string.Compare(x.WeekNumber, request.ToWeek) <= 0);
}
if (request.OnlyCalculated.HasValue && request.OnlyCalculated.Value)
{
query = query.Where(x => x.IsCalculated);
}
// Order by week number descending (newest first)
query = query.OrderByDescending(x => x.WeekNumber);
// Count total
var totalCount = await query.CountAsync(cancellationToken);
// Paginate
var pools = await query
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.Select(x => new WeeklyCommissionPoolDto
{
Id = x.Id,
WeekNumber = x.WeekNumber,
TotalPoolAmount = x.TotalPoolAmount,
TotalBalances = x.TotalBalances,
ValuePerBalance = x.ValuePerBalance,
IsCalculated = x.IsCalculated,
CalculatedAt = x.CalculatedAt,
Created = x.Created
})
.ToListAsync(cancellationToken);
return new GetAllWeeklyPoolsResponseDto
{
MetaData = new MetaDataDto
{
TotalCount = totalCount,
PageSize = request.PageSize,
CurrentPage = request.PageIndex,
TotalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize)
},
Models = pools
};
}
}

View File

@@ -0,0 +1,27 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
public record GetAllWeeklyPoolsResponseDto
{
public MetaDataDto MetaData { get; init; } = new();
public List<WeeklyCommissionPoolDto> Models { get; init; } = new();
}
public record WeeklyCommissionPoolDto
{
public long Id { get; init; }
public string WeekNumber { get; init; } = string.Empty;
public long TotalPoolAmount { get; init; }
public int TotalBalances { get; init; }
public long ValuePerBalance { get; init; }
public bool IsCalculated { get; init; }
public DateTime? CalculatedAt { get; init; }
public DateTime Created { get; init; }
}
public record MetaDataDto
{
public int TotalCount { get; init; }
public int PageSize { get; init; }
public int CurrentPage { get; init; }
public int TotalPages { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
public class GetWithdrawalRequestsQuery : IRequest<GetWithdrawalRequestsResponseDto>
{
public int? Status { get; set; } // CommissionPayoutStatus enum
public long? UserId { get; set; }
public string? WeekNumber { get; set; }
public PaginationState? PaginationState { get; set; }
public string? SortBy { get; set; }
}

View File

@@ -0,0 +1,67 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRequestsQuery, GetWithdrawalRequestsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetWithdrawalRequestsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetWithdrawalRequestsResponseDto> Handle(GetWithdrawalRequestsQuery request, CancellationToken cancellationToken)
{
var query = _context.UserCommissionPayouts
.AsNoTracking()
.Include(x => x.User)
.Where(x => x.WithdrawalMethod != null) // Only requests with withdrawal method
.AsQueryable();
// Filters
if (request.Status.HasValue)
{
query = query.Where(x => (int)x.Status == request.Status.Value);
}
if (request.UserId.HasValue)
{
query = query.Where(x => x.UserId == request.UserId.Value);
}
if (!string.IsNullOrEmpty(request.WeekNumber))
{
query = query.Where(x => x.WeekNumber == request.WeekNumber);
}
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.ToListAsync(cancellationToken);
var result = models.Select(x => new WithdrawalRequestModel
{
Id = x.Id,
UserId = x.UserId,
UserName = x.User != null ? (x.User.FirstName + " " + x.User.LastName).Trim() : x.User?.Mobile ?? "N/A",
WeekNumber = x.WeekNumber,
Amount = x.TotalAmount,
Status = (int)x.Status,
WithdrawalMethod = x.WithdrawalMethod.HasValue ? (int)x.WithdrawalMethod.Value : 0,
IbanNumber = x.IbanNumber,
RequestedAt = x.WithdrawnAt ?? x.Created,
ProcessedAt = x.LastModified,
ProcessedBy = null, // TODO: Add admin user tracking
Reason = null, // TODO: Add rejection reason field
Created = x.Created
}).ToList();
return new GetWithdrawalRequestsResponseDto
{
MetaData = meta,
Models = result
};
}
}

View File

@@ -0,0 +1,24 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
public class GetWithdrawalRequestsResponseDto
{
public MetaData? MetaData { get; set; }
public List<WithdrawalRequestModel> Models { get; set; } = new();
}
public class WithdrawalRequestModel
{
public long Id { get; set; }
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string WeekNumber { get; set; } = string.Empty;
public long Amount { get; set; }
public int Status { get; set; } // CommissionPayoutStatus enum
public int? WithdrawalMethod { get; set; }
public string? IbanNumber { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? ProcessedAt { get; set; }
public string? ProcessedBy { get; set; }
public string? Reason { get; set; }
public DateTime Created { get; set; }
}

View File

@@ -0,0 +1,13 @@
using CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
public record GetWorkerExecutionLogsQuery : IRequest<GetWorkerExecutionLogsResponseDto>
{
public string? WeekNumber { get; init; }
public string? ExecutionId { get; init; }
public bool? SuccessOnly { get; init; }
public bool? FailedOnly { get; init; }
public string? SortBy { get; init; }
public PaginationState? PaginationState { get; init; }
}

View File

@@ -0,0 +1,106 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecutionLogsQuery, GetWorkerExecutionLogsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetWorkerExecutionLogsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetWorkerExecutionLogsResponseDto> Handle(
GetWorkerExecutionLogsQuery request,
CancellationToken cancellationToken)
{
// TODO: این باید از یک entity واقعی لاگ‌ها را بگیرد
// فعلاً mock data برمی‌گرداند
await Task.CompletedTask;
var mockLogs = new List<WorkerExecutionLogModel>
{
new WorkerExecutionLogModel
{
ExecutionId = Guid.NewGuid().ToString(),
WeekNumber = "2025-W48",
Step = "Full",
Success = true,
ErrorMessage = null,
StartedAt = DateTime.UtcNow.AddHours(-24),
CompletedAt = DateTime.UtcNow.AddHours(-24).AddMinutes(15),
DurationMs = 900000, // 15 minutes
RecordsProcessed = 1523,
Details = "محاسبات کامل هفته 2025-W48 با موفقیت انجام شد"
},
new WorkerExecutionLogModel
{
ExecutionId = Guid.NewGuid().ToString(),
WeekNumber = "2025-W47",
Step = "Full",
Success = true,
ErrorMessage = null,
StartedAt = DateTime.UtcNow.AddDays(-7),
CompletedAt = DateTime.UtcNow.AddDays(-7).AddMinutes(12),
DurationMs = 720000,
RecordsProcessed = 1489,
Details = "محاسبات کامل هفته 2025-W47 با موفقیت انجام شد"
},
new WorkerExecutionLogModel
{
ExecutionId = Guid.NewGuid().ToString(),
WeekNumber = "2025-W46",
Step = "Pool",
Success = false,
ErrorMessage = "خطا در محاسبه استخر کمیسیون",
StartedAt = DateTime.UtcNow.AddDays(-14),
CompletedAt = DateTime.UtcNow.AddDays(-14).AddSeconds(30),
DurationMs = 30000,
RecordsProcessed = 0,
Details = "محاسبه استخر با خطا مواجه شد"
}
};
// Apply filters
if (!string.IsNullOrEmpty(request.WeekNumber))
{
mockLogs = mockLogs.Where(x => x.WeekNumber == request.WeekNumber).ToList();
}
if (request.SuccessOnly == true)
{
mockLogs = mockLogs.Where(x => x.Success).ToList();
}
if (request.FailedOnly == true)
{
mockLogs = mockLogs.Where(x => !x.Success).ToList();
}
var totalCount = mockLogs.Count;
var pageSize = request.PaginationState?.PageSize ?? 10;
var pageNumber = request.PaginationState?.PageNumber ?? 1;
var pagedLogs = mockLogs
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
return new GetWorkerExecutionLogsResponseDto
{
MetaData = new MetaData
{
CurrentPage = pageNumber,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
PageSize = pageSize,
TotalCount = totalCount,
HasPrevious = pageNumber > 1,
HasNext = pageNumber < (int)Math.Ceiling(totalCount / (double)pageSize)
},
Models = pagedLogs
};
}
}

View File

@@ -0,0 +1,23 @@
using CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
public class GetWorkerExecutionLogsResponseDto
{
public MetaData? MetaData { get; set; }
public List<WorkerExecutionLogModel> Models { get; set; } = new();
}
public class WorkerExecutionLogModel
{
public string ExecutionId { get; set; } = string.Empty;
public string WeekNumber { get; set; } = string.Empty;
public string Step { get; set; } = string.Empty; // "Balances" | "Pool" | "Payouts" | "Full"
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public long DurationMs { get; set; }
public int RecordsProcessed { get; set; }
public string? Details { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
public record GetWorkerStatusQuery : IRequest<GetWorkerStatusResponseDto>
{
// Empty - returns current worker status
}

View File

@@ -0,0 +1,37 @@
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
public class GetWorkerStatusQueryHandler : IRequestHandler<GetWorkerStatusQuery, GetWorkerStatusResponseDto>
{
private readonly IApplicationDbContext _context;
public GetWorkerStatusQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetWorkerStatusResponseDto> Handle(
GetWorkerStatusQuery request,
CancellationToken cancellationToken)
{
// TODO: این باید از یک service یا cache واقعی worker status را بگیرد
// فعلاً mock data برمی‌گرداند
await Task.CompletedTask;
return new GetWorkerStatusResponseDto
{
IsRunning = false,
IsEnabled = true,
CurrentExecutionId = null,
CurrentWeekNumber = null,
CurrentStep = "Idle",
LastRunAt = DateTime.UtcNow.AddHours(-24),
NextScheduledRun = DateTime.UtcNow.AddDays(7),
TotalExecutions = 48,
SuccessfulExecutions = 47,
FailedExecutions = 1
};
}
}

View File

@@ -0,0 +1,15 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
public class GetWorkerStatusResponseDto
{
public bool IsRunning { get; set; }
public bool IsEnabled { get; set; }
public string? CurrentExecutionId { get; set; }
public string? CurrentWeekNumber { get; set; }
public string? CurrentStep { get; set; } // "Balances" | "Pool" | "Payouts" | "Idle"
public DateTime? LastRunAt { get; set; }
public DateTime? NextScheduledRun { get; set; }
public int TotalExecutions { get; set; }
public int SuccessfulExecutions { get; set; }
public int FailedExecutions { get; set; }
}

View File

@@ -0,0 +1,19 @@
namespace CMSMicroservice.Application.Common.Exceptions;
public class BadRequestException : Exception
{
public BadRequestException()
: base()
{
}
public BadRequestException(string message)
: base(message)
{
}
public BadRequestException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -0,0 +1,6 @@
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
public class GetNetworkStatisticsQuery : IRequest<GetNetworkStatisticsResponseDto>
{
// No parameters - returns overall statistics
}

View File

@@ -0,0 +1,110 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatisticsQuery, GetNetworkStatisticsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetNetworkStatisticsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
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);
var leftLegCount = await _context.Users
.Where(x => x.LegPosition == NetworkLeg.Left)
.CountAsync(cancellationToken);
var rightLegCount = await _context.Users
.Where(x => x.LegPosition == NetworkLeg.Right)
.CountAsync(cancellationToken);
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)
{
// 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) }
};
}
// Monthly growth (last 6 months) - using Created date
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6);
var monthlyGrowth = await _context.Users
.Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo)
.GroupBy(x => new { x.Created.Year, x.Created.Month })
.Select(g => new MonthlyGrowthModel
{
Month = $"{g.Key.Year}-{g.Key.Month:D2}",
NewMembers = g.Count()
})
.OrderBy(x => x.Month)
.ToListAsync(cancellationToken);
// Top users by total children count
var topUsers = await _context.Users
.Where(x => x.NetworkParentId != null)
.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)
})
.Where(x => x.LeftCount + x.RightCount > 0)
.OrderByDescending(x => x.LeftCount + x.RightCount)
.Take(10)
.ToListAsync(cancellationToken);
var topUserModels = topUsers.Select((x, index) => new TopNetworkUserModel
{
Rank = index + 1,
UserId = x.Id,
UserName = x.UserName,
TotalChildren = x.LeftCount + x.RightCount,
LeftCount = x.LeftCount,
RightCount = x.RightCount
}).ToList();
return new GetNetworkStatisticsResponseDto
{
TotalMembers = totalMembers,
ActiveMembers = activeMembers,
LeftLegCount = leftLegCount,
RightLegCount = rightLegCount,
LeftPercentage = leftPercentage,
RightPercentage = rightPercentage,
AverageDepth = averageDepth,
MaxDepth = maxDepth,
LevelDistribution = levelDistribution,
MonthlyGrowth = monthlyGrowth,
TopUsers = topUserModels
};
}
}

View File

@@ -0,0 +1,38 @@
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
public class GetNetworkStatisticsResponseDto
{
public int TotalMembers { get; set; }
public int ActiveMembers { get; set; }
public int LeftLegCount { get; set; }
public int RightLegCount { get; set; }
public double LeftPercentage { get; set; }
public double RightPercentage { get; set; }
public double AverageDepth { get; set; }
public int MaxDepth { get; set; }
public List<LevelDistributionModel> LevelDistribution { get; set; } = new();
public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new();
public List<TopNetworkUserModel> TopUsers { get; set; } = new();
}
public class LevelDistributionModel
{
public int Level { get; set; }
public int Count { get; set; }
}
public class MonthlyGrowthModel
{
public string Month { get; set; } = string.Empty;
public int NewMembers { get; set; }
}
public class TopNetworkUserModel
{
public int Rank { get; set; }
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public int TotalChildren { get; set; }
public int LeftCount { get; set; }
public int RightCount { get; set; }
}

View File

@@ -17,7 +17,7 @@ public class UserOrder : BaseAuditableEntity
//Transaction Navigation Property //Transaction Navigation Property
public virtual Transactions? Transaction { get; set; } public virtual Transactions? Transaction { get; set; }
//وضعیت پرداخت //وضعیت پرداخت
public PaymentStatus PaymentStatus { get; set; } public PaymentStatus PaymentStatus { get; set; }
//تاریخ پرداخت //تاریخ پرداخت
public DateTime? PaymentDate { get; set; } public DateTime? PaymentDate { get; set; }
//شناسه کاربر //شناسه کاربر