feat: Add ClubMembershipCQ - Phase 3 Application Layer

- Implemented 3 Commands with handlers and validators:
  * ActivateClubMembership: Create/reactivate membership
  * DeactivateClubMembership: Deactivate existing membership
  * AssignClubFeature: Assign features to club members

- Implemented 3 Queries with handlers, validators, and DTOs:
  * GetClubMembership: Retrieve single membership by UserId
  * GetAllClubMemberships: List with filtering and pagination
  * GetClubMembershipHistory: Audit trail with full history

- All operations include history tracking via ClubMembershipHistory
- Property names aligned with Domain entity definitions
- 21 new files, ~600 lines of code
- Build successful with 0 errors
This commit is contained in:
masoodafar-web
2025-11-29 04:13:27 +03:30
parent fb911cd0fd
commit fe66d478ee
21 changed files with 732 additions and 0 deletions
@@ -0,0 +1,45 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
/// <summary>
/// Query برای دریافت لیست عضویت‌های باشگاه
/// </summary>
public record GetAllClubMembershipsQuery : IRequest<GetAllClubMembershipsResponseDto>
{
/// <summary>
/// موقعیت صفحه‌بندی
/// </summary>
public PaginationState? PaginationState { get; init; }
/// <summary>
/// مرتب‌سازی بر اساس
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// فیلتر
/// </summary>
public GetAllClubMembershipsFilter? Filter { get; init; }
}
public class GetAllClubMembershipsFilter
{
/// <summary>
/// فیلتر بر اساس شناسه کاربر
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// فقط عضویت‌های فعال
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// فیلتر بر اساس تاریخ فعال‌سازی (از)
/// </summary>
public DateTimeOffset? ActivationDateFrom { get; set; }
/// <summary>
/// فیلتر بر اساس تاریخ فعال‌سازی (تا)
/// </summary>
public DateTimeOffset? ActivationDateTo { get; set; }
}
@@ -0,0 +1,51 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
public class GetAllClubMembershipsQueryHandler : IRequestHandler<GetAllClubMembershipsQuery, GetAllClubMembershipsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAllClubMembershipsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAllClubMembershipsResponseDto> Handle(GetAllClubMembershipsQuery request, CancellationToken cancellationToken)
{
var query = _context.ClubMemberships
.ApplyOrder(sortBy: request.SortBy)
.AsNoTracking()
.AsQueryable();
if (request.Filter is not null)
{
query = query
.Where(x => request.Filter.UserId == null || x.UserId == request.Filter.UserId)
.Where(x => request.Filter.IsActive == null || x.IsActive == request.Filter.IsActive)
.Where(x => request.Filter.ActivationDateFrom == null || x.ActivatedAt >= request.Filter.ActivationDateFrom)
.Where(x => request.Filter.ActivationDateTo == null || x.ActivatedAt <= request.Filter.ActivationDateTo);
}
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetAllClubMembershipsResponseModel
{
Id = x.Id,
UserId = x.UserId,
IsActive = x.IsActive,
ActivatedAt = x.ActivatedAt,
InitialContribution = x.InitialContribution,
TotalEarned = x.TotalEarned,
Created = x.Created,
LastModified = x.LastModified
})
.ToListAsync(cancellationToken);
return new GetAllClubMembershipsResponseDto
{
MetaData = meta,
Models = models
};
}
}
@@ -0,0 +1,30 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
public class GetAllClubMembershipsQueryValidator : AbstractValidator<GetAllClubMembershipsQuery>
{
public GetAllClubMembershipsQueryValidator()
{
RuleFor(x => x.Filter.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست")
.When(x => x.Filter?.UserId != null);
RuleFor(x => x.Filter.ActivationDateTo)
.GreaterThanOrEqualTo(x => x.Filter.ActivationDateFrom)
.WithMessage("تاریخ پایان باید بعد از تاریخ شروع باشد")
.When(x => x.Filter?.ActivationDateFrom != null && x.Filter?.ActivationDateTo != null);
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetAllClubMembershipsQuery>.CreateWithOptions(
(GetAllClubMembershipsQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -0,0 +1,19 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
public class GetAllClubMembershipsResponseDto
{
public MetaData MetaData { get; set; }
public List<GetAllClubMembershipsResponseModel> Models { get; set; }
}
public class GetAllClubMembershipsResponseModel
{
public long Id { get; set; }
public long UserId { get; set; }
public bool IsActive { get; set; }
public DateTime? ActivatedAt { get; set; }
public long InitialContribution { get; set; }
public long TotalEarned { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? LastModified { get; set; }
}
@@ -0,0 +1,16 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
/// <summary>
/// DTO برای نمایش اطلاعات عضویت باشگاه
/// </summary>
public class ClubMembershipDto
{
public long Id { get; set; }
public long UserId { get; set; }
public bool IsActive { get; set; }
public DateTime? ActivatedAt { get; set; }
public long InitialContribution { get; set; }
public long TotalEarned { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? LastModified { get; set; }
}
@@ -0,0 +1,12 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
/// <summary>
/// Query برای دریافت عضویت باشگاه یک کاربر
/// </summary>
public record GetClubMembershipQuery : IRequest<ClubMembershipDto?>
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
}
@@ -0,0 +1,32 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
{
private readonly IApplicationDbContext _context;
public GetClubMembershipQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
{
var membership = await _context.ClubMemberships
.AsNoTracking()
.Where(x => x.UserId == request.UserId)
.Select(x => new ClubMembershipDto
{
Id = x.Id,
UserId = x.UserId,
IsActive = x.IsActive,
ActivatedAt = x.ActivatedAt,
InitialContribution = x.InitialContribution,
TotalEarned = x.TotalEarned,
Created = x.Created,
LastModified = x.LastModified
})
.FirstOrDefaultAsync(cancellationToken);
return membership;
}
}
@@ -0,0 +1,24 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
public class GetClubMembershipQueryValidator : AbstractValidator<GetClubMembershipQuery>
{
public GetClubMembershipQueryValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetClubMembershipQuery>.CreateWithOptions(
(GetClubMembershipQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -0,0 +1,27 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
/// <summary>
/// Query برای دریافت تاریخچه تغییرات عضویت باشگاه
/// </summary>
public record GetClubMembershipHistoryQuery : IRequest<GetClubMembershipHistoryResponseDto>
{
/// <summary>
/// شناسه عضویت (اختیاری)
/// </summary>
public long? MembershipId { get; init; }
/// <summary>
/// شناسه کاربر (اختیاری)
/// </summary>
public long? UserId { get; init; }
/// <summary>
/// موقعیت صفحه‌بندی
/// </summary>
public PaginationState? PaginationState { get; init; }
/// <summary>
/// مرتب‌سازی بر اساس
/// </summary>
public string? SortBy { get; init; }
}
@@ -0,0 +1,56 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
public class GetClubMembershipHistoryQueryHandler : IRequestHandler<GetClubMembershipHistoryQuery, GetClubMembershipHistoryResponseDto>
{
private readonly IApplicationDbContext _context;
public GetClubMembershipHistoryQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetClubMembershipHistoryResponseDto> Handle(GetClubMembershipHistoryQuery request, CancellationToken cancellationToken)
{
var query = _context.ClubMembershipHistories
.AsNoTracking()
.AsQueryable();
if (request.MembershipId.HasValue)
{
query = query.Where(x => x.ClubMembershipId == request.MembershipId.Value);
}
if (request.UserId.HasValue)
{
query = query.Where(x => x.UserId == request.UserId.Value);
}
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created"); // پیش‌فرض: جدیدترین اول
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetClubMembershipHistoryResponseModel
{
Id = x.Id,
ClubMembershipId = x.ClubMembershipId,
UserId = x.UserId,
OldIsActive = x.OldIsActive,
NewIsActive = x.NewIsActive,
OldInitialContribution = x.OldInitialContribution,
NewInitialContribution = x.NewInitialContribution,
Action = x.Action,
Reason = x.Reason,
PerformedBy = x.PerformedBy,
Created = x.Created
})
.ToListAsync(cancellationToken);
return new GetClubMembershipHistoryResponseDto
{
MetaData = meta,
Models = models
};
}
}
@@ -0,0 +1,34 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
public class GetClubMembershipHistoryQueryValidator : AbstractValidator<GetClubMembershipHistoryQuery>
{
public GetClubMembershipHistoryQueryValidator()
{
RuleFor(x => x.MembershipId)
.GreaterThan(0)
.WithMessage("شناسه عضویت معتبر نیست")
.When(x => x.MembershipId.HasValue);
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست")
.When(x => x.UserId.HasValue);
RuleFor(x => x)
.Must(x => x.MembershipId.HasValue || x.UserId.HasValue)
.WithMessage("حداقل یکی از MembershipId یا UserId باید مقداردهی شود");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetClubMembershipHistoryQuery>.CreateWithOptions(
(GetClubMembershipHistoryQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -0,0 +1,22 @@
namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHistory;
public class GetClubMembershipHistoryResponseDto
{
public MetaData MetaData { get; set; }
public List<GetClubMembershipHistoryResponseModel> Models { get; set; }
}
public class GetClubMembershipHistoryResponseModel
{
public long Id { get; set; }
public long ClubMembershipId { get; set; }
public long UserId { get; set; }
public bool OldIsActive { get; set; }
public bool NewIsActive { get; set; }
public long? OldInitialContribution { get; set; }
public long? NewInitialContribution { get; set; }
public ClubMembershipAction Action { get; set; }
public string? Reason { get; set; }
public string? PerformedBy { get; set; }
public DateTimeOffset Created { get; set; }
}