diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs index aa5b3f4..90f23e4 100644 --- a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs +++ b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Options; using PnvPanel.Application.Common.Interfaces; +using PnvPanel.Domain.Support; using PnvPanel.Infrastructure.Telegram; using Telegram.Bot; using Telegram.Bot.Types.Enums; @@ -97,6 +98,35 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } } + public async Task NotifyAdminsTicketReopenedAsync(Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(options.Value.BotToken)) + return; + + var typeLabel = type == TicketType.RoleRequest ? "заявка на роль" : "баг/предложение"; + var text = $"🔓 Тикет от {Escape(userName)} переоткрыт ({typeLabel})"; + + InlineKeyboardMarkup? keyboard = null; + if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) + { + var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/support?ticket={ticketId}"; + keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) }); + } + + foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) + { + try + { + await botClient.SendMessage( + adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + } + catch + { + // Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду. + } + } + } + public async Task NotifyUsersNewsPublishedAsync(string title, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs index 84437df..a37de20 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs @@ -1,3 +1,5 @@ +using PnvPanel.Domain.Support; + namespace PnvPanel.Application.Common.Interfaces; /// @@ -25,4 +27,8 @@ public interface ITelegramNotifier /// Рассылка о публикации новости всем активированным пользователям с привязанным Telegram /// (кнопка-ссылка на сайт, где новость показана целиком). Task NotifyUsersNewsPublishedAsync(string title, CancellationToken cancellationToken); + + /// Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без + /// инлайн-действий (аналогично баг-репортам). + Task NotifyAdminsTicketReopenedAsync(Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken); } diff --git a/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs index 754949c..dd4faec 100644 --- a/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs @@ -7,7 +7,7 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.Reopen; -public sealed class ReopenTicketCommandHandler(IAppDbContext dbContext, ICurrentUser currentUser) +public sealed class ReopenTicketCommandHandler(IAppDbContext dbContext, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) : ICommandHandler { public async Task Handle(ReopenTicketCommand command, CancellationToken cancellationToken) @@ -24,6 +24,10 @@ public sealed class ReopenTicketCommandHandler(IAppDbContext dbContext, ICurrent return Result.Failure(SupportErrors.NotResolved); ticket.Reopen(); + + var userName = currentUser.UserName ?? userId.ToString(); + await telegramNotifier.NotifyAdminsTicketReopenedAsync(ticket.Id, userName, ticket.Type, cancellationToken); + return Result.Success(); } } diff --git a/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs index 48524cf..fbe28fe 100644 --- a/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs @@ -1,3 +1,5 @@ +using NSubstitute; +using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Support; using PnvPanel.Application.Support.Reopen; using PnvPanel.Application.Tests.TestSupport; @@ -8,6 +10,8 @@ namespace PnvPanel.Application.Tests.Support; public class ReopenTicketCommandHandlerTests { + private readonly ITelegramNotifier _telegramNotifier = Substitute.For(); + [Fact] public async Task Handle_WhenResolved_SetsOpen() { @@ -19,12 +23,14 @@ public class ReopenTicketCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var currentUser = FakeCurrentUser.Authenticated(userId); - var handler = new ReopenTicketCommandHandler(dbContext, currentUser); + var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser); var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(TicketStatus.Open, ticket.Status); + await _telegramNotifier.Received(1).NotifyAdminsTicketReopenedAsync( + ticket.Id, Arg.Any(), TicketType.BugReport, Arg.Any()); } [Fact] @@ -37,7 +43,7 @@ public class ReopenTicketCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var currentUser = FakeCurrentUser.Authenticated(userId); - var handler = new ReopenTicketCommandHandler(dbContext, currentUser); + var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser); var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None); @@ -55,7 +61,7 @@ public class ReopenTicketCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); - var handler = new ReopenTicketCommandHandler(dbContext, currentUser); + var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser); var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None);