feat: add support tickets and imrpove web app loading

This commit is contained in:
3252a8
2026-05-20 16:38:24 +03:00
parent 3b846f0d44
commit d8a1da1f13
78 changed files with 8990 additions and 168 deletions
+366
View File
@@ -0,0 +1,366 @@
import asyncio
from types import SimpleNamespace
from bot.services.notification_service import NotificationService
from config.settings import Settings
def _settings(**overrides):
data = {
"BOT_TOKEN": "123456:test",
"POSTGRES_USER": "app_user",
"POSTGRES_PASSWORD": "app_password",
}
data.update(overrides)
return Settings(_env_file=None, **data)
def test_support_ticket_url_uses_subscription_mini_app_url():
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.com"),
)
assert service._support_ticket_url(42, admin=True) == "https://app.example.com/admin/support/42"
assert service._support_ticket_url(42, admin=False) == "https://app.example.com/support/42"
def test_support_ticket_url_falls_back_to_startapp_deeplink():
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(),
bot_username="demo_bot",
)
assert service._support_ticket_url(42) == "https://t.me/demo_bot?startapp=ticket_42"
def test_admin_support_keyboard_uses_consistent_admin_links():
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.com/app"),
)
ticket = SimpleNamespace(ticket_id=42)
user = SimpleNamespace(user_id=100200300)
keyboard = service._support_keyboard(ticket, user, admin=True)
ticket_button = keyboard.inline_keyboard[0][0]
user_card_button = keyboard.inline_keyboard[1][1]
assert keyboard.inline_keyboard[0][0].text == "Открыть тикет"
assert ticket_button.url is None
assert ticket_button.web_app.url == "https://app.example.com/app/admin/support/42"
assert keyboard.inline_keyboard[1][0].url == "tg://user?id=100200300"
assert user_card_button.url is None
assert user_card_button.web_app.url == "https://app.example.com/app/admin/users/100200300"
def test_admin_support_keyboard_falls_back_to_startapp_url():
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(),
bot_username="demo_bot",
)
ticket = SimpleNamespace(ticket_id=42)
user = SimpleNamespace(user_id=100200300)
keyboard = service._support_keyboard(ticket, user, admin=True)
button = keyboard.inline_keyboard[0][0]
assert button.web_app is None
assert button.url == "https://t.me/demo_bot?startapp=ticket_42"
def test_user_support_keyboard_uses_web_app_button_when_configured():
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.com/app"),
)
ticket = SimpleNamespace(ticket_id=42)
user = SimpleNamespace(language_code="ru")
keyboard = service._support_user_keyboard(ticket, user)
button = keyboard.inline_keyboard[0][0]
assert button.text == "Открыть тикет"
assert button.url is None
assert button.web_app.url == "https://app.example.com/app/support/42"
def test_user_support_keyboard_falls_back_to_startapp_url():
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(),
bot_username="demo_bot",
)
ticket = SimpleNamespace(ticket_id=42)
user = SimpleNamespace(language_code="ru")
keyboard = service._support_user_keyboard(ticket, user)
button = keyboard.inline_keyboard[0][0]
assert button.text == "Открыть тикет"
assert button.web_app is None
assert button.url == "https://t.me/demo_bot?startapp=ticket_42"
def test_admin_support_email_notifications_can_be_disabled():
sent = []
class EmailService:
async def send_rendered_email(self, *, email, content):
sent.append((email, content))
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=False),
email_auth_service=EmailService(),
)
async def admin_email_users():
return [SimpleNamespace(user_id=1, email="admin@example.com", language_code="en")]
service._admin_email_users = admin_email_users
async def run():
await service._send_admin_support_email(
lambda *_args, **_kwargs: SimpleNamespace(subject="Ticket", html="Body", text="Body"),
ticket_id=1,
)
asyncio.run(run())
assert sent == []
def test_admin_support_email_notifications_default_to_disabled():
sent = []
class EmailService:
async def send_rendered_email(self, *, email, content):
sent.append((email, content))
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(),
email_auth_service=EmailService(),
)
async def admin_email_users():
return [SimpleNamespace(user_id=1, email="admin@example.com", language_code="en")]
service._admin_email_users = admin_email_users
async def run():
await service._send_admin_support_email(
lambda *_args, **_kwargs: SimpleNamespace(subject="Ticket", html="Body", text="Body"),
ticket_id=1,
)
asyncio.run(run())
assert sent == []
def test_persisted_support_email_override_disables_env_enabled(monkeypatch):
sent = []
class EmailService:
async def send_rendered_email(self, *, email, content):
sent.append((email, content))
class SessionFactory:
def __call__(self):
return self
async def __aenter__(self):
return SimpleNamespace()
async def __aexit__(self, exc_type, exc, tb):
return None
async def get_override_value(_session, key):
assert key == "SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED"
return True, False
monkeypatch.setattr(
"bot.services.notification_service.app_settings_dal.get_override_value",
get_override_value,
)
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=True),
session_factory=SessionFactory(),
email_auth_service=EmailService(),
)
async def admin_email_users():
return [SimpleNamespace(user_id=1, email="admin@example.com", language_code="en")]
service._admin_email_users = admin_email_users
async def run():
await service._send_admin_support_email(
lambda *_args, **_kwargs: SimpleNamespace(subject="Ticket", html="Body", text="Body"),
ticket_id=1,
)
asyncio.run(run())
assert sent == []
def test_persisted_support_email_override_enables_default_disabled(monkeypatch):
sent = []
class EmailService:
async def send_rendered_email(self, *, email, content):
sent.append((email, content))
class SessionFactory:
def __call__(self):
return self
async def __aenter__(self):
return SimpleNamespace()
async def __aexit__(self, exc_type, exc, tb):
return None
async def get_override_value(_session, key):
assert key == "SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED"
return True, True
monkeypatch.setattr(
"bot.services.notification_service.app_settings_dal.get_override_value",
get_override_value,
)
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(),
session_factory=SessionFactory(),
email_auth_service=EmailService(),
)
async def admin_email_users():
return [SimpleNamespace(user_id=1, email="admin@example.com", language_code="en")]
service._admin_email_users = admin_email_users
async def run():
await service._send_admin_support_email(
lambda *_args, **_kwargs: SimpleNamespace(subject="Ticket", html="Body", text="Body"),
ticket_id=1,
)
asyncio.run(run())
assert len(sent) == 1
def test_disabled_admin_support_email_keeps_telegram_and_log_notifications():
emails = []
channels = []
class EmailService:
async def send_rendered_email(self, *, email, content):
emails.append((email, content))
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(
SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=False,
SUBSCRIPTION_MINI_APP_URL="https://app.example.com",
),
email_auth_service=EmailService(),
)
async def send_to_admins(message, reply_markup=None):
channels.append(("admins", bool(message), bool(reply_markup)))
async def send_to_log_channel(message, thread_id=None, reply_markup=None):
channels.append(("log", bool(message), bool(reply_markup)))
service._send_to_admins = send_to_admins
service._send_to_log_channel = send_to_log_channel
ticket = SimpleNamespace(
ticket_id=7,
priority="normal",
category="technical",
subject="Connection issue",
)
user = SimpleNamespace(
user_id=100200300,
username="user",
first_name="User",
last_name=None,
email="user@example.com",
)
asyncio.run(
service.notify_new_support_ticket(
ticket,
user,
"Cannot connect",
{"tariff": "Standard", "end_date": "2026-06-01"},
)
)
assert [item[0] for item in channels] == ["admins", "log"]
assert emails == []
def test_support_user_reply_can_send_email_without_telegram_channels():
emails = []
channels = []
service = NotificationService(
bot=SimpleNamespace(),
settings=_settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.com"),
)
async def send_to_admins(message, reply_markup=None):
channels.append(("admins", bool(message), bool(reply_markup)))
async def send_to_log_channel(message, thread_id=None, reply_markup=None):
channels.append(("log", bool(message), bool(reply_markup)))
async def send_admin_support_email(renderer, **kwargs):
emails.append(kwargs)
service._send_to_admins = send_to_admins
service._send_to_log_channel = send_to_log_channel
service._send_admin_support_email = send_admin_support_email
ticket = SimpleNamespace(
ticket_id=7,
priority="normal",
category="technical",
subject="Connection issue",
)
message = SimpleNamespace(body="Still cannot connect")
user = SimpleNamespace(
user_id=100200300,
username="user",
first_name="User",
last_name=None,
email="user@example.com",
)
asyncio.run(
service.notify_support_user_reply(
ticket,
message,
user,
{},
unread_count=3,
send_telegram=False,
send_email=True,
)
)
assert channels == []
assert emails[0]["ticket_id"] == 7