feat: add support tickets and imrpove web app loading
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from bot.app.web.admin_settings_manifest import manifest_payload
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
SUPPORT_RELATED_SETTINGS = (
|
||||
"LOG_SUPPORT_THREAD_ID",
|
||||
"SUPPORT_TICKETS_ENABLED",
|
||||
"SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED",
|
||||
"SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS",
|
||||
"SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS",
|
||||
"SUPPORT_TICKET_MAX_BODY_LENGTH",
|
||||
"SUPPORT_TICKET_MAX_SUBJECT_LENGTH",
|
||||
"SUPPORT_TICKET_RATE_LIMIT_PER_HOUR",
|
||||
)
|
||||
|
||||
|
||||
def _manifest_by_key() -> dict[str, dict]:
|
||||
return {item["key"]: item for item in manifest_payload()}
|
||||
|
||||
|
||||
def _locale(language: str) -> dict[str, str]:
|
||||
return json.loads((REPO_ROOT / "locales" / f"{language}.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_support_settings_manifest_uses_admin_i18n_keys():
|
||||
manifest = _manifest_by_key()
|
||||
|
||||
assert manifest["SUPPORT_TICKETS_ENABLED"]["section"] == "support"
|
||||
assert manifest["SUPPORT_TICKETS_ENABLED"]["section_order"] == 8
|
||||
|
||||
for setting_key in SUPPORT_RELATED_SETTINGS:
|
||||
field = manifest[setting_key]
|
||||
prefix = f"admin_settings_field_{setting_key.lower()}"
|
||||
|
||||
assert field["i18n_label_key"] == f"{prefix}_label"
|
||||
assert field["i18n_description_key"] == f"{prefix}_description"
|
||||
|
||||
|
||||
def test_support_settings_i18n_keys_exist_in_admin_locales():
|
||||
manifest = _manifest_by_key()
|
||||
|
||||
for language in ("ru", "en"):
|
||||
messages = _locale(language)
|
||||
|
||||
assert "admin_settings_section_support" in messages
|
||||
for setting_key in SUPPORT_RELATED_SETTINGS:
|
||||
field = manifest[setting_key]
|
||||
assert field["i18n_label_key"] in messages
|
||||
assert field["i18n_description_key"] in messages
|
||||
@@ -18,7 +18,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from bot.app.factories.build_services import build_core_services
|
||||
from bot.payment_providers.yookassa import YooKassaService
|
||||
@@ -26,7 +26,6 @@ from bot.services.panel_webhook_service import PanelWebhookService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
# Strip all provider env so per-provider BaseSettings models don't pick up
|
||||
# real credentials from the local .env file during tests.
|
||||
_PROVIDER_ENV_PREFIXES = (
|
||||
@@ -140,6 +139,9 @@ class BuildServicesWiringTests(unittest.TestCase):
|
||||
"subscription_service",
|
||||
"referral_service",
|
||||
"promo_code_service",
|
||||
"notification_service",
|
||||
"email_auth_service",
|
||||
"support_service",
|
||||
"stars_service",
|
||||
"cryptopay_service",
|
||||
"freekassa_service",
|
||||
|
||||
@@ -152,11 +152,22 @@ class SettingsTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(settings.TRIAL_TRAFFIC_STRATEGY, "WEEK")
|
||||
|
||||
def test_support_admin_email_notifications_default_to_disabled(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
)
|
||||
|
||||
self.assertFalse(settings.SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED)
|
||||
|
||||
def test_payment_button_presentation_env_values_are_available(self):
|
||||
"""Presentation overrides now live on each provider's BaseSettings
|
||||
model instead of the central Settings — verify they're loaded from
|
||||
env and exposed via the provider bundle."""
|
||||
import os
|
||||
|
||||
from bot.payment_providers import build_provider_configs, get_spec_presentation
|
||||
|
||||
os.environ["PAYMENT_YOOKASSA_WEBAPP_LABEL_RU"] = "Карта"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from bot.app.web.admin_api_impl.support import AdminTicketPatchPayload, AdminTicketReplyPayload
|
||||
|
||||
|
||||
def test_admin_patch_payload_accepts_closed_status_and_urgent_priority():
|
||||
payload = AdminTicketPatchPayload.model_validate(
|
||||
{"status": "closed", "priority": "urgent", "category": "billing"}
|
||||
)
|
||||
|
||||
assert payload.status == "closed"
|
||||
assert payload.priority == "urgent"
|
||||
|
||||
|
||||
def test_admin_reply_payload_supports_internal_note():
|
||||
payload = AdminTicketReplyPayload.model_validate({"body": " note ", "is_internal_note": True})
|
||||
|
||||
assert payload.body == "note"
|
||||
assert payload.is_internal_note is True
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from bot.app.web.webapp.payloads import CreateTicketPayload, TicketReplyPayload
|
||||
|
||||
|
||||
def test_user_ticket_payload_accepts_only_public_priorities():
|
||||
payload = CreateTicketPayload.model_validate(
|
||||
{"subject": "Help", "category": "technical", "priority": "high", "body": "Text"}
|
||||
)
|
||||
|
||||
assert payload.priority == "high"
|
||||
|
||||
|
||||
def test_user_ticket_payload_rejects_admin_only_priority():
|
||||
with pytest.raises(ValidationError):
|
||||
CreateTicketPayload.model_validate(
|
||||
{"subject": "Help", "category": "technical", "priority": "urgent", "body": "Text"}
|
||||
)
|
||||
|
||||
|
||||
def test_ticket_reply_trims_body():
|
||||
payload = TicketReplyPayload.model_validate({"body": " hello "})
|
||||
|
||||
assert payload.body == "hello"
|
||||
@@ -0,0 +1,12 @@
|
||||
from db.dal import support_dal
|
||||
|
||||
|
||||
def test_support_dal_status_groups_are_future_close_ready():
|
||||
assert "closed" in support_dal.CLOSED_STATUSES
|
||||
assert "resolved" in support_dal.CLOSED_STATUSES
|
||||
assert "awaiting_admin" in support_dal.ACTIVE_STATUSES
|
||||
|
||||
|
||||
def test_support_dal_all_status_filter_means_no_filter():
|
||||
assert support_dal._status_condition("all") is None
|
||||
assert support_dal._status_condition("any") is None
|
||||
@@ -0,0 +1,23 @@
|
||||
from db.migrator import MIGRATIONS
|
||||
from db.models import SupportTicket, SupportTicketMessage
|
||||
|
||||
|
||||
def test_support_migration_is_registered_after_existing_revisions():
|
||||
ids = [migration.id for migration in MIGRATIONS]
|
||||
|
||||
assert "0024_add_support_tickets" in ids
|
||||
assert ids.index("0024_add_support_tickets") > ids.index("0023_add_email_password_auth_fields")
|
||||
assert "0025_add_support_notification_timestamps" in ids
|
||||
assert ids.index("0025_add_support_notification_timestamps") > ids.index(
|
||||
"0024_add_support_tickets"
|
||||
)
|
||||
|
||||
|
||||
def test_support_models_expose_expected_tables():
|
||||
assert SupportTicket.__tablename__ == "support_tickets"
|
||||
assert SupportTicketMessage.__tablename__ == "support_ticket_messages"
|
||||
assert "admin_last_notified_at" in SupportTicket.__table__.columns
|
||||
assert "admin_last_emailed_at" in SupportTicket.__table__.columns
|
||||
assert "ix_support_tickets_status_last_msg" in {
|
||||
index.name for index in SupportTicket.__table__.indexes
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,102 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
from bot.services.support_service import (
|
||||
SupportService,
|
||||
TicketForbidden,
|
||||
_support_admin_notification_decision,
|
||||
)
|
||||
|
||||
|
||||
def test_support_traffic_snapshot_calculates_percent_and_left_bytes():
|
||||
snapshot = SupportService._traffic_snapshot(25, 100)
|
||||
|
||||
assert snapshot["percent"] == 25
|
||||
assert snapshot["left_bytes"] == 75
|
||||
|
||||
|
||||
def test_ticket_forbidden_error_code_is_stable():
|
||||
exc = TicketForbidden("ticket_forbidden")
|
||||
|
||||
assert str(exc) == "ticket_forbidden"
|
||||
|
||||
|
||||
def test_regular_limit_treats_unlimited_override_as_zero_limit():
|
||||
sub = SimpleNamespace(regular_unlimited_override=True, traffic_limit_bytes=100)
|
||||
|
||||
assert SupportService._regular_limit(sub) == 0
|
||||
|
||||
|
||||
def test_support_admin_notification_decision_sends_first_unread():
|
||||
now = datetime(2026, 5, 20, tzinfo=timezone.utc)
|
||||
ticket = SimpleNamespace(
|
||||
unread_admin_count=1,
|
||||
admin_last_notified_at=now,
|
||||
admin_last_emailed_at=now,
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS=300,
|
||||
SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS=1800,
|
||||
SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=True,
|
||||
)
|
||||
|
||||
decision = _support_admin_notification_decision(ticket, settings, now=now)
|
||||
|
||||
assert decision.send_telegram is True
|
||||
assert decision.send_email is True
|
||||
|
||||
|
||||
def test_support_admin_notification_decision_defaults_email_disabled():
|
||||
now = datetime(2026, 5, 20, tzinfo=timezone.utc)
|
||||
ticket = SimpleNamespace(
|
||||
unread_admin_count=1,
|
||||
admin_last_notified_at=None,
|
||||
admin_last_emailed_at=None,
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS=300,
|
||||
SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS=1800,
|
||||
)
|
||||
|
||||
decision = _support_admin_notification_decision(ticket, settings, now=now)
|
||||
|
||||
assert decision.send_telegram is True
|
||||
assert decision.send_email is False
|
||||
|
||||
|
||||
def test_support_admin_notification_decision_suppresses_fast_followups():
|
||||
now = datetime(2026, 5, 20, tzinfo=timezone.utc)
|
||||
ticket = SimpleNamespace(
|
||||
unread_admin_count=4,
|
||||
admin_last_notified_at=now - timedelta(seconds=60),
|
||||
admin_last_emailed_at=now - timedelta(seconds=60),
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS=300,
|
||||
SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS=1800,
|
||||
SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=True,
|
||||
)
|
||||
|
||||
decision = _support_admin_notification_decision(ticket, settings, now=now)
|
||||
|
||||
assert decision.send_telegram is False
|
||||
assert decision.send_email is False
|
||||
|
||||
|
||||
def test_support_admin_notification_decision_uses_separate_email_cooldown():
|
||||
now = datetime(2026, 5, 20, tzinfo=timezone.utc)
|
||||
ticket = SimpleNamespace(
|
||||
unread_admin_count=4,
|
||||
admin_last_notified_at=now - timedelta(seconds=301),
|
||||
admin_last_emailed_at=now - timedelta(seconds=301),
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS=300,
|
||||
SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS=1800,
|
||||
SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=True,
|
||||
)
|
||||
|
||||
decision = _support_admin_notification_decision(ticket, settings, now=now)
|
||||
|
||||
assert decision.send_telegram is True
|
||||
assert decision.send_email is False
|
||||
Reference in New Issue
Block a user