using Microsoft.EntityFrameworkCore; using PnvPanel.Application.Auth; using PnvPanel.Application.Common.Concurrency; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Messaging; using PnvPanel.Application.Common.Models; using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.CreateRoleRequest; /// Проверка "нет висящей заявки" + создание — под AdvisoryLock (по UserId), см. /// CreatePaymentRequestCommandHandler для полного обоснования. public sealed class CreateRoleRequestTicketCommandHandler( IAppDbContext dbContext, IRoleService roleService, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser ) : ICommandHandler> { // Совпадает со значением Infrastructure.Identity.RoleNames.Admin — Application не может ссылаться // на Infrastructure (направление зависимостей), поэтому системное имя роли продублировано здесь. private const string AdminRoleName = "admin"; public async Task> Handle( CreateRoleRequestTicketCommand command, CancellationToken cancellationToken ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); string? requestedRoleName = null; if (command.ExistingRoleId is { } existingRoleId) { var roles = await roleService.ListRolesAsync(cancellationToken); var role = roles.FirstOrDefault(r => r.Id == existingRoleId); if (role is null) return Result.Failure(SupportErrors.RoleNotFound); if (role.Name.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase)) return Result.Failure(SupportErrors.CannotRequestAdminRole); requestedRoleName = role.Name; } var claimed = await AdvisoryLock.RunAsync( dbContext, userId, async lockedCancellationToken => { var hasPending = await dbContext.SupportTickets.AnyAsync( t => t.UserId == userId && t.Type == TicketType.RoleRequest && t.Status == TicketStatus.Open, lockedCancellationToken ); if (hasPending) return Result.Failure<(SupportTicket, TicketComment)>( SupportErrors.RoleRequestAlreadyPending ); var newTicket = command.ExistingRoleId is { } roleId ? SupportTicket.CreateRoleRequestForExistingRole(userId, roleId) : SupportTicket.CreateRoleRequestForNewRole( userId, command.NewRoleName!, command.NewRoleMaxConfigs!.Value, command.NewRoleMaxIpLimit!.Value ); dbContext.SupportTickets.Add(newTicket); var newComment = TicketComment.Create(newTicket.Id, userId, command.Justification); dbContext.TicketComments.Add(newComment); return Result.Success((newTicket, newComment)); }, cancellationToken ); if (!claimed.IsSuccess) return Result.Failure(claimed.Error); var (ticket, comment) = claimed.Value; var userName = currentUser.UserName ?? userId.ToString(); var roleDescription = requestedRoleName ?? $"новая роль «{command.NewRoleName}» (конфигов: {command.NewRoleMaxConfigs}, IP: {command.NewRoleMaxIpLimit})"; await notifier.NotifyTicketCreatedAsync( ticket.Id, userId, userName, ticket.Type, cancellationToken ); await telegramNotifier.NotifyAdminsRoleRequestCreatedAsync( ticket.Id, userName, roleDescription, command.Justification, cancellationToken ); var commentDto = new TicketCommentDto( comment.Id, userId, userName, comment.Body, comment.CreatedAt, [] ); var dto = new TicketDetailDto( ticket.Id, ticket.UserId, userName, ticket.Type, ticket.Status, ticket.RequestedRoleId, requestedRoleName, ticket.ProposedRoleName, ticket.ProposedMaxConfigs, ticket.ProposedMaxIpLimit, ticket.RequestedDays, ticket.CreatedAt, [commentDto] ); return Result.Success(dto); } }