From db96a02f89e50ee46ed55c79efb12580f3f104de Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 29 Nov 2025 04:19:40 +0330 Subject: [PATCH] feat: Add NetworkMembershipCQ - Phase 4 Application Layer - Implemented 3 Commands with handlers and validators: * JoinNetworkCommand: Add user to binary network tree - Validates parent exists and is in network - Validates leg position is empty - Records history with NetworkMembershipAction.Join * MoveInNetworkCommand: Move user to different position - Validates new parent and leg availability - Prevents circular dependencies (IsDescendant check) - Records old/new parent and leg in history * RemoveFromNetworkCommand: Remove user from network - Validates no children exist (must move/remove first) - Soft delete (sets NetworkParentId to null) - Idempotent design - Implemented 3 Queries with handlers, validators, and DTOs: * GetNetworkTreeQuery: Binary tree visualization - Recursive tree building with MaxDepth limit (1-10) - Returns nested structure with Left/Right children * GetUserNetworkPositionQuery: User position details - Parent info, leg position, children counts - Left/Right child counts for balance view * GetNetworkMembershipHistoryQuery: Complete audit trail - Filter by UserId, pagination support - Shows Join/Move/Remove actions with full details - All operations include complete history tracking - Binary tree validation (parent-child relationships) - Circular dependency prevention in MoveInNetwork - 21 new files, ~850 lines of code - Build successful with 0 errors --- .../JoinNetwork/JoinNetworkCommand.cs | 27 +++++ .../JoinNetwork/JoinNetworkCommandHandler.cs | 80 +++++++++++++ .../JoinNetworkCommandValidator.cs | 37 ++++++ .../MoveInNetwork/MoveInNetworkCommand.cs | 27 +++++ .../MoveInNetworkCommandHandler.cs | 108 ++++++++++++++++++ .../MoveInNetworkCommandValidator.cs | 37 ++++++ .../RemoveFromNetworkCommand.cs | 17 +++ .../RemoveFromNetworkCommandHandler.cs | 69 +++++++++++ .../RemoveFromNetworkCommandValidator.cs | 29 +++++ .../GetNetworkMembershipHistoryQuery.cs | 22 ++++ ...GetNetworkMembershipHistoryQueryHandler.cs | 50 ++++++++ ...tNetworkMembershipHistoryQueryValidator.cs | 25 ++++ .../GetNetworkMembershipHistoryResponseDto.cs | 21 ++++ .../GetNetworkTree/GetNetworkTreeQuery.cs | 17 +++ .../GetNetworkTreeQueryHandler.cs | 78 +++++++++++++ .../GetNetworkTreeQueryValidator.cs | 28 +++++ .../Queries/GetNetworkTree/NetworkTreeDto.cs | 16 +++ .../GetUserNetworkPositionQuery.cs | 12 ++ .../GetUserNetworkPositionQueryHandler.cs | 70 ++++++++++++ .../GetUserNetworkPositionQueryValidator.cs | 24 ++++ .../UserNetworkPositionDto.cs | 19 +++ 21 files changed, 813 insertions(+) create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommand.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandHandler.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandValidator.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommand.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandHandler.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandValidator.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommand.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandHandler.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandValidator.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQuery.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryHandler.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryValidator.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryResponseDto.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryValidator.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQuery.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryHandler.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryValidator.cs create mode 100644 src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/UserNetworkPositionDto.cs diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommand.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommand.cs new file mode 100644 index 0000000..3da1d9e --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommand.cs @@ -0,0 +1,27 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.JoinNetwork; + +/// +/// Command برای افزودن کاربر به شبکه دوتایی (Binary Network) +/// +public record JoinNetworkCommand : IRequest +{ + /// + /// شناسه کاربر که می‌خواهد به شبکه بپیوندد + /// + public long UserId { get; init; } + + /// + /// شناسه والد در شبکه (Sponsor/Parent) + /// + public long ParentId { get; init; } + + /// + /// موقعیت در شبکه (Left یا Right) + /// + public NetworkLeg LegPosition { get; init; } + + /// + /// دلیل/یادداشت (اختیاری) + /// + public string? Reason { get; init; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandHandler.cs new file mode 100644 index 0000000..69521b2 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandHandler.cs @@ -0,0 +1,80 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.JoinNetwork; + +public class JoinNetworkCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public JoinNetworkCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(JoinNetworkCommand request, CancellationToken cancellationToken) + { + // بررسی وجود کاربر + var user = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (user == null) + { + throw new NotFoundException(nameof(User), request.UserId); + } + + // بررسی اینکه کاربر قبلاً در شبکه نباشد + if (user.NetworkParentId.HasValue) + { + throw new InvalidOperationException($"کاربر با شناسه {request.UserId} قبلاً در شبکه عضو شده است"); + } + + // بررسی وجود والد + var parent = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.ParentId, cancellationToken); + + if (parent == null) + { + throw new NotFoundException(nameof(User), $"Parent with Id {request.ParentId}"); + } + + // بررسی والد خودش در شبکه باشد (یا Root باشد) + if (!parent.NetworkParentId.HasValue && parent.Id != 1) // فرض: UserId=1 همیشه Root + { + throw new InvalidOperationException($"والد با شناسه {request.ParentId} خودش در شبکه عضو نیست"); + } + + // بررسی خالی بودن Leg موردنظر + var legOccupied = await _context.Users + .AnyAsync(x => x.NetworkParentId == request.ParentId && x.LegPosition == request.LegPosition, + cancellationToken); + + if (legOccupied) + { + throw new InvalidOperationException( + $"موقعیت {request.LegPosition} زیر والد {request.ParentId} قبلاً پر شده است"); + } + + // افزودن به شبکه + user.NetworkParentId = request.ParentId; + user.LegPosition = request.LegPosition; + + _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + + // ثبت تاریخچه + var history = new NetworkMembershipHistory + { + UserId = request.UserId, + OldParentId = null, + NewParentId = request.ParentId, + OldLegPosition = null, + NewLegPosition = request.LegPosition, + Action = NetworkMembershipAction.Join, + Reason = request.Reason ?? "عضویت در شبکه", + PerformedBy = "System" // TODO: باید از Current User گرفته شود + }; + + await _context.NetworkMembershipHistories.AddAsync(history, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandValidator.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandValidator.cs new file mode 100644 index 0000000..533502d --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/JoinNetwork/JoinNetworkCommandValidator.cs @@ -0,0 +1,37 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.JoinNetwork; + +public class JoinNetworkCommandValidator : AbstractValidator +{ + public JoinNetworkCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر معتبر نیست"); + + RuleFor(x => x.ParentId) + .GreaterThan(0) + .WithMessage("شناسه والد معتبر نیست"); + + RuleFor(x => x.LegPosition) + .IsInEnum() + .WithMessage("موقعیت شبکه باید Left یا Right باشد"); + + RuleFor(x => x.Reason) + .MaximumLength(500) + .WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد") + .When(x => !string.IsNullOrEmpty(x.Reason)); + } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync( + ValidationContext.CreateWithOptions( + (JoinNetworkCommand)model, + x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommand.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommand.cs new file mode 100644 index 0000000..a6a113e --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommand.cs @@ -0,0 +1,27 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.MoveInNetwork; + +/// +/// Command برای جابجایی کاربر در شبکه دوتایی +/// +public record MoveInNetworkCommand : IRequest +{ + /// + /// شناسه کاربر که می‌خواهد جابجا شود + /// + public long UserId { get; init; } + + /// + /// شناسه والد جدید در شبکه + /// + public long NewParentId { get; init; } + + /// + /// موقعیت جدید در شبکه (Left یا Right) + /// + public NetworkLeg NewLegPosition { get; init; } + + /// + /// دلیل جابجایی + /// + public string? Reason { get; init; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandHandler.cs new file mode 100644 index 0000000..ae6c77a --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandHandler.cs @@ -0,0 +1,108 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.MoveInNetwork; + +public class MoveInNetworkCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public MoveInNetworkCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(MoveInNetworkCommand request, CancellationToken cancellationToken) + { + // بررسی وجود کاربر + var user = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (user == null) + { + throw new NotFoundException(nameof(User), request.UserId); + } + + // بررسی اینکه کاربر در شبکه باشد + if (!user.NetworkParentId.HasValue) + { + throw new InvalidOperationException($"کاربر با شناسه {request.UserId} در شبکه عضو نیست"); + } + + // بررسی وجود والد جدید + var newParent = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.NewParentId, cancellationToken); + + if (newParent == null) + { + throw new NotFoundException(nameof(User), $"New Parent with Id {request.NewParentId}"); + } + + // بررسی اینکه والد جدید خود کاربر یا فرزندان او نباشد (جلوگیری از Loop) + if (await IsDescendant(request.NewParentId, request.UserId, cancellationToken)) + { + throw new InvalidOperationException("نمی‌توان کاربر را زیر فرزندان خودش جابجا کرد (ایجاد حلقه)"); + } + + // بررسی خالی بودن Leg جدید + var legOccupied = await _context.Users + .AnyAsync(x => x.NetworkParentId == request.NewParentId && + x.LegPosition == request.NewLegPosition && + x.Id != request.UserId, + cancellationToken); + + if (legOccupied) + { + throw new InvalidOperationException( + $"موقعیت {request.NewLegPosition} زیر والد {request.NewParentId} قبلاً پر شده است"); + } + + // ذخیره مقادیر قبلی برای History + var oldParentId = user.NetworkParentId; + var oldLegPosition = user.LegPosition; + + // جابجایی + user.NetworkParentId = request.NewParentId; + user.LegPosition = request.NewLegPosition; + + _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + + // ثبت تاریخچه + var history = new NetworkMembershipHistory + { + UserId = request.UserId, + OldParentId = oldParentId, + NewParentId = request.NewParentId, + OldLegPosition = oldLegPosition, + NewLegPosition = request.NewLegPosition, + Action = NetworkMembershipAction.Move, + Reason = request.Reason ?? "جابجایی در شبکه", + PerformedBy = "System" // TODO: باید از Current User گرفته شود + }; + + await _context.NetworkMembershipHistories.AddAsync(history, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } + + /// + /// بررسی می‌کند که آیا potentialDescendant فرزند (مستقیم یا غیرمستقیم) userId هست یا خیر + /// + private async Task IsDescendant(long potentialDescendantId, long userId, CancellationToken cancellationToken) + { + var current = await _context.Users + .FirstOrDefaultAsync(x => x.Id == potentialDescendantId, cancellationToken); + + while (current?.NetworkParentId != null) + { + if (current.NetworkParentId == userId) + { + return true; + } + + current = await _context.Users + .FirstOrDefaultAsync(x => x.Id == current.NetworkParentId, cancellationToken); + } + + return false; + } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandValidator.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandValidator.cs new file mode 100644 index 0000000..2a483c7 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/MoveInNetwork/MoveInNetworkCommandValidator.cs @@ -0,0 +1,37 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.MoveInNetwork; + +public class MoveInNetworkCommandValidator : AbstractValidator +{ + public MoveInNetworkCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر معتبر نیست"); + + RuleFor(x => x.NewParentId) + .GreaterThan(0) + .WithMessage("شناسه والد جدید معتبر نیست"); + + RuleFor(x => x.NewLegPosition) + .IsInEnum() + .WithMessage("موقعیت شبکه باید Left یا Right باشد"); + + RuleFor(x => x.Reason) + .MaximumLength(500) + .WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد") + .When(x => !string.IsNullOrEmpty(x.Reason)); + } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync( + ValidationContext.CreateWithOptions( + (MoveInNetworkCommand)model, + x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommand.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommand.cs new file mode 100644 index 0000000..f2aa053 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommand.cs @@ -0,0 +1,17 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.RemoveFromNetwork; + +/// +/// Command برای حذف کاربر از شبکه دوتایی +/// +public record RemoveFromNetworkCommand : IRequest +{ + /// + /// شناسه کاربر که باید از شبکه حذف شود + /// + public long UserId { get; init; } + + /// + /// دلیل حذف + /// + public string? Reason { get; init; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandHandler.cs new file mode 100644 index 0000000..4fac7ff --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandHandler.cs @@ -0,0 +1,69 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.RemoveFromNetwork; + +public class RemoveFromNetworkCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public RemoveFromNetworkCommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(RemoveFromNetworkCommand request, CancellationToken cancellationToken) + { + // بررسی وجود کاربر + var user = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (user == null) + { + throw new NotFoundException(nameof(User), request.UserId); + } + + // بررسی اینکه کاربر در شبکه باشد + if (!user.NetworkParentId.HasValue) + { + // اگر قبلاً حذف شده، هیچ کاری نکن (Idempotent) + return Unit.Value; + } + + // بررسی وجود فرزندان + var hasChildren = await _context.Users + .AnyAsync(x => x.NetworkParentId == request.UserId, cancellationToken); + + if (hasChildren) + { + throw new InvalidOperationException( + $"کاربر با شناسه {request.UserId} دارای فرزند در شبکه است. ابتدا باید فرزندان جابجا یا حذف شوند"); + } + + // ذخیره مقادیر قبلی برای History + var oldParentId = user.NetworkParentId; + var oldLegPosition = user.LegPosition; + + // حذف از شبکه (Soft Delete) + user.NetworkParentId = null; + user.LegPosition = null; + + _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + + // ثبت تاریخچه + var history = new NetworkMembershipHistory + { + UserId = request.UserId, + OldParentId = oldParentId, + NewParentId = null, + OldLegPosition = oldLegPosition, + NewLegPosition = null, + Action = NetworkMembershipAction.Remove, + Reason = request.Reason ?? "حذف از شبکه", + PerformedBy = "System" // TODO: باید از Current User گرفته شود + }; + + await _context.NetworkMembershipHistories.AddAsync(history, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandValidator.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandValidator.cs new file mode 100644 index 0000000..1b32f57 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/RemoveFromNetwork/RemoveFromNetworkCommandValidator.cs @@ -0,0 +1,29 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.RemoveFromNetwork; + +public class RemoveFromNetworkCommandValidator : AbstractValidator +{ + public RemoveFromNetworkCommandValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر معتبر نیست"); + + RuleFor(x => x.Reason) + .MaximumLength(500) + .WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد") + .When(x => !string.IsNullOrEmpty(x.Reason)); + } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync( + ValidationContext.CreateWithOptions( + (RemoveFromNetworkCommand)model, + x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQuery.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQuery.cs new file mode 100644 index 0000000..e7be2f8 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQuery.cs @@ -0,0 +1,22 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory; + +/// +/// Query برای دریافت تاریخچه تغییرات شبکه یک کاربر +/// +public record GetNetworkMembershipHistoryQuery : IRequest +{ + /// + /// شناسه کاربر (اختیاری) + /// + public long? UserId { get; init; } + + /// + /// مرتب‌سازی (پیش‌فرض: -Created) + /// + public string? SortBy { get; init; } + + /// + /// Pagination + /// + public PaginationState? PaginationState { get; init; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryHandler.cs new file mode 100644 index 0000000..2e2548a --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryHandler.cs @@ -0,0 +1,50 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory; + +public class GetNetworkMembershipHistoryQueryHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public GetNetworkMembershipHistoryQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(GetNetworkMembershipHistoryQuery request, CancellationToken cancellationToken) + { + var query = _context.NetworkMembershipHistories + .AsNoTracking() + .AsQueryable(); + + 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 GetNetworkMembershipHistoryResponseModel + { + Id = x.Id, + UserId = x.UserId, + OldParentId = x.OldParentId, + NewParentId = x.NewParentId, + OldLegPosition = x.OldLegPosition, + NewLegPosition = x.NewLegPosition, + Action = x.Action, + Reason = x.Reason, + PerformedBy = x.PerformedBy, + Created = x.Created + }) + .ToListAsync(cancellationToken); + + return new GetNetworkMembershipHistoryResponseDto + { + MetaData = meta, + Models = models + }; + } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryValidator.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryValidator.cs new file mode 100644 index 0000000..68f38c2 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryQueryValidator.cs @@ -0,0 +1,25 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory; + +public class GetNetworkMembershipHistoryQueryValidator : AbstractValidator +{ + public GetNetworkMembershipHistoryQueryValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر معتبر نیست") + .When(x => x.UserId.HasValue); + } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync( + ValidationContext.CreateWithOptions( + (GetNetworkMembershipHistoryQuery)model, + x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryResponseDto.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryResponseDto.cs new file mode 100644 index 0000000..b3d266b --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkMembershipHistory/GetNetworkMembershipHistoryResponseDto.cs @@ -0,0 +1,21 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory; + +public class GetNetworkMembershipHistoryResponseDto +{ + public MetaData MetaData { get; set; } + public List Models { get; set; } +} + +public class GetNetworkMembershipHistoryResponseModel +{ + public long Id { get; set; } + public long UserId { get; set; } + public long? OldParentId { get; set; } + public long? NewParentId { get; set; } + public NetworkLeg? OldLegPosition { get; set; } + public NetworkLeg? NewLegPosition { get; set; } + public NetworkMembershipAction Action { get; set; } + public string? Reason { get; set; } + public string? PerformedBy { get; set; } + public DateTimeOffset Created { get; set; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs new file mode 100644 index 0000000..1386bb3 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQuery.cs @@ -0,0 +1,17 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree; + +/// +/// Query برای دریافت درخت شبکه از یک کاربر (Binary Tree) +/// +public record GetNetworkTreeQuery : IRequest +{ + /// + /// شناسه کاربر که می‌خواهیم درخت زیرمجموعه او را ببینیم + /// + public long UserId { get; init; } + + /// + /// تعداد سطوح (Depth) که می‌خواهیم نمایش دهیم (پیش‌فرض: 3) + /// + public int MaxDepth { get; init; } = 3; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs new file mode 100644 index 0000000..067775c --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs @@ -0,0 +1,78 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree; + +public class GetNetworkTreeQueryHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public GetNetworkTreeQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken) + { + var rootUser = await _context.Users + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (rootUser == null) + { + return null; + } + + var tree = await BuildTree(rootUser.Id, request.MaxDepth, 0, cancellationToken); + return tree; + } + + private async Task BuildTree(long userId, int maxDepth, int currentDepth, CancellationToken cancellationToken) + { + var user = await _context.Users + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); + + if (user == null) + { + throw new NotFoundException(nameof(User), userId); + } + + var node = new NetworkTreeDto + { + UserId = user.Id, + Mobile = user.Mobile, + FirstName = user.FirstName, + LastName = user.LastName, + LegPosition = user.LegPosition, + CurrentDepth = currentDepth + }; + + // اگر به حداکثر عمق رسیدیم، دیگر فرزندان را نمی‌خوانیم + if (currentDepth >= maxDepth) + { + return node; + } + + // پیدا کردن فرزند چپ + var leftChild = await _context.Users + .AsNoTracking() + .FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left, + cancellationToken); + + if (leftChild != null) + { + node.LeftChild = await BuildTree(leftChild.Id, maxDepth, currentDepth + 1, cancellationToken); + } + + // پیدا کردن فرزند راست + var rightChild = await _context.Users + .AsNoTracking() + .FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right, + cancellationToken); + + if (rightChild != null) + { + node.RightChild = await BuildTree(rightChild.Id, maxDepth, currentDepth + 1, cancellationToken); + } + + return node; + } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryValidator.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryValidator.cs new file mode 100644 index 0000000..3e78a58 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryValidator.cs @@ -0,0 +1,28 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree; + +public class GetNetworkTreeQueryValidator : AbstractValidator +{ + public GetNetworkTreeQueryValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر معتبر نیست"); + + RuleFor(x => x.MaxDepth) + .InclusiveBetween(1, 10) + .WithMessage("عمق درخت باید بین 1 تا 10 باشد"); + } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync( + ValidationContext.CreateWithOptions( + (GetNetworkTreeQuery)model, + x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs new file mode 100644 index 0000000..c623693 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs @@ -0,0 +1,16 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree; + +/// +/// DTO برای نمایش درخت دوتایی شبکه +/// +public class NetworkTreeDto +{ + public long UserId { get; set; } + public string? Mobile { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public NetworkLeg? LegPosition { get; set; } + public int CurrentDepth { get; set; } + public NetworkTreeDto? LeftChild { get; set; } + public NetworkTreeDto? RightChild { get; set; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQuery.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQuery.cs new file mode 100644 index 0000000..f7eba62 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQuery.cs @@ -0,0 +1,12 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition; + +/// +/// Query برای دریافت موقعیت کاربر در شبکه +/// +public record GetUserNetworkPositionQuery : IRequest +{ + /// + /// شناسه کاربر + /// + public long UserId { get; init; } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryHandler.cs new file mode 100644 index 0000000..42c1294 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryHandler.cs @@ -0,0 +1,70 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition; + +public class GetUserNetworkPositionQueryHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + + public GetUserNetworkPositionQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(GetUserNetworkPositionQuery request, CancellationToken cancellationToken) + { + var user = await _context.Users + .AsNoTracking() + .Where(x => x.Id == request.UserId) + .Select(x => new + { + x.Id, + x.Mobile, + x.FirstName, + x.LastName, + x.NetworkParentId, + x.LegPosition + }) + .FirstOrDefaultAsync(cancellationToken); + + if (user == null) + { + return null; + } + + // شمارش فرزندان + var childrenCount = await _context.Users + .CountAsync(x => x.NetworkParentId == request.UserId, cancellationToken); + + var leftChildCount = await _context.Users + .CountAsync(x => x.NetworkParentId == request.UserId && x.LegPosition == NetworkLeg.Left, + cancellationToken); + + var rightChildCount = await _context.Users + .CountAsync(x => x.NetworkParentId == request.UserId && x.LegPosition == NetworkLeg.Right, + cancellationToken); + + // اطلاعات والد + string? parentMobile = null; + if (user.NetworkParentId.HasValue) + { + parentMobile = await _context.Users + .Where(x => x.Id == user.NetworkParentId) + .Select(x => x.Mobile) + .FirstOrDefaultAsync(cancellationToken); + } + + return new UserNetworkPositionDto + { + UserId = user.Id, + Mobile = user.Mobile, + FirstName = user.FirstName, + LastName = user.LastName, + NetworkParentId = user.NetworkParentId, + ParentMobile = parentMobile, + LegPosition = user.LegPosition, + TotalChildren = childrenCount, + LeftChildCount = leftChildCount, + RightChildCount = rightChildCount, + IsInNetwork = user.NetworkParentId.HasValue + }; + } +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryValidator.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryValidator.cs new file mode 100644 index 0000000..8064192 --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/GetUserNetworkPositionQueryValidator.cs @@ -0,0 +1,24 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition; + +public class GetUserNetworkPositionQueryValidator : AbstractValidator +{ + public GetUserNetworkPositionQueryValidator() + { + RuleFor(x => x.UserId) + .GreaterThan(0) + .WithMessage("شناسه کاربر معتبر نیست"); + } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync( + ValidationContext.CreateWithOptions( + (GetUserNetworkPositionQuery)model, + x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; +} diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/UserNetworkPositionDto.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/UserNetworkPositionDto.cs new file mode 100644 index 0000000..d3a292b --- /dev/null +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserNetworkPosition/UserNetworkPositionDto.cs @@ -0,0 +1,19 @@ +namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition; + +/// +/// DTO برای نمایش موقعیت کاربر در شبکه +/// +public class UserNetworkPositionDto +{ + public long UserId { get; set; } + public string? Mobile { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public long? NetworkParentId { get; set; } + public string? ParentMobile { get; set; } + public NetworkLeg? LegPosition { get; set; } + public int TotalChildren { get; set; } + public int LeftChildCount { get; set; } + public int RightChildCount { get; set; } + public bool IsInNetwork { get; set; } +}