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);