fix: localize support email templates

This commit is contained in:
3252a8
2026-05-31 14:41:26 +03:00
parent e4ef52df8b
commit eda5d3e633
6 changed files with 429 additions and 37 deletions
+71 -33
View File
@@ -89,6 +89,7 @@ def _t_text(i18n: JsonI18n, lang: str, key: str, **kwargs) -> str:
def _layout( def _layout(
*, *,
settings: Settings, settings: Settings,
language_code: str,
preheader: str, preheader: str,
heading: str, heading: str,
intro_html: str, intro_html: str,
@@ -98,6 +99,7 @@ def _layout(
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR) accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
brand_title = html.escape(_brand_title(settings)) brand_title = html.escape(_brand_title(settings))
logo_url = _public_logo_url(settings) logo_url = _public_logo_url(settings)
html_lang = html.escape((language_code or "en").replace("_", "-"), quote=True)
logo_block = "" logo_block = ""
if logo_url: if logo_url:
logo_block = ( logo_block = (
@@ -107,7 +109,7 @@ def _layout(
) )
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <html lang="{html_lang}" xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -307,6 +309,7 @@ def render_login_code(
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=preheader, preheader=preheader,
heading=heading, heading=heading,
intro_html=html.escape(intro), intro_html=html.escape(intro),
@@ -359,6 +362,7 @@ def render_account_merged(
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=preheader, preheader=preheader,
heading=heading, heading=heading,
intro_html=html.escape(intro), intro_html=html.escape(intro),
@@ -492,6 +496,7 @@ def render_payment_success(
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=preheader, preheader=preheader,
heading=heading, heading=heading,
intro_html=html.escape(intro), intro_html=html.escape(intro),
@@ -542,6 +547,7 @@ def render_user_notification(
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=final_subject, preheader=final_subject,
heading=final_heading, heading=final_heading,
intro_html=html.escape(final_intro), intro_html=html.escape(final_intro),
@@ -615,6 +621,7 @@ def render_subscription_expiring(
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=preheader, preheader=preheader,
heading=heading, heading=heading,
intro_html=html.escape(intro), intro_html=html.escape(intro),
@@ -702,6 +709,7 @@ def render_subscription_lifecycle_notification(
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=subject, preheader=subject,
heading=subject, heading=subject,
intro_html=html.escape(intro), intro_html=html.escape(intro),
@@ -738,28 +746,40 @@ def _support_email(
ticket_url: Optional[str], ticket_url: Optional[str],
cta_label: str, cta_label: str,
) -> EmailContent: ) -> EmailContent:
i18n = _resolve_i18n(i18n)
lang = _normalize_lang(language, settings) lang = _normalize_lang(language, settings)
brand = _brand_title(settings) brand = _brand_title(settings)
accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR) accent = _safe_color(settings.WEBAPP_PRIMARY_COLOR)
safe_url = (ticket_url or "").strip() safe_url = (ticket_url or "").strip()
footer = _t_html(_resolve_i18n(i18n), lang, "email_footer_auto", brand=brand) footer = _t_html(i18n, lang, "email_footer_auto", brand=brand)
localized_rows = [
(_t_text(i18n, lang, label) if str(label).startswith("email_") else str(label), value)
for label, value in rows
]
preview_block = ( preview_block = (
f'<div style="margin:0 0 16px 0;background:{_BG};border:1px solid {_BORDER};' f'<div style="margin:0 0 16px 0;background:{_BG};border:1px solid {_BORDER};'
f"border-radius:14px;padding:14px 16px;font-size:14px;line-height:1.55;color:{_TEXT};" f"border-radius:14px;padding:14px 16px;font-size:14px;line-height:1.55;color:{_TEXT};"
f'white-space:pre-wrap;">{html.escape(body_preview or "")}</div>' f'white-space:pre-wrap;">{html.escape(body_preview or "")}</div>'
) )
body_parts = [_info_rows_html(rows), preview_block] body_parts = [_info_rows_html(localized_rows), preview_block]
if safe_url: if safe_url:
body_parts.append(_cta_button_html(label=cta_label, url=safe_url, accent=accent)) body_parts.append(_cta_button_html(label=cta_label, url=safe_url, accent=accent))
rendered = _layout( rendered = _layout(
settings=settings, settings=settings,
language_code=lang,
preheader=intro, preheader=intro,
heading=heading, heading=heading,
intro_html=html.escape(intro), intro_html=html.escape(intro),
body_html="".join(body_parts), body_html="".join(body_parts),
footer_html=footer, footer_html=footer,
) )
text_lines = [intro, "", *[f"{label}: {value}" for label, value in rows], "", body_preview] text_lines = [
intro,
"",
*[f"{label}: {value}" for label, value in localized_rows],
"",
body_preview,
]
if safe_url: if safe_url:
text_lines.extend(["", safe_url]) text_lines.extend(["", safe_url])
return EmailContent(subject=subject, text="\n".join(text_lines), html=rendered) return EmailContent(subject=subject, text="\n".join(text_lines), html=rendered)
@@ -777,23 +797,25 @@ def render_support_new_ticket_admin(
snapshot_rows: Sequence[Tuple[str, str]], snapshot_rows: Sequence[Tuple[str, str]],
ticket_url: Optional[str], ticket_url: Optional[str],
) -> EmailContent: ) -> EmailContent:
i18n = _resolve_i18n(i18n)
lang = _normalize_lang(language, settings)
rows = [ rows = [
("Ticket", f"#{ticket_id}"), ("email_support_row_ticket", f"#{ticket_id}"),
("User", user_display), ("email_support_row_user", user_display),
("Subject", subject), ("email_support_row_subject", subject),
*snapshot_rows, *snapshot_rows,
] ]
return _support_email( return _support_email(
settings, settings,
i18n, i18n,
language, lang,
subject=f"New support ticket #{ticket_id}", subject=_t_text(i18n, lang, "email_support_new_ticket_admin_subject", ticket_id=ticket_id),
heading=f"New support ticket #{ticket_id}", heading=_t_text(i18n, lang, "email_support_new_ticket_admin_heading", ticket_id=ticket_id),
intro="A user opened a new support ticket.", intro=_t_text(i18n, lang, "email_support_new_ticket_admin_intro"),
rows=rows, rows=rows,
body_preview=body_preview, body_preview=body_preview,
ticket_url=ticket_url, ticket_url=ticket_url,
cta_label="Open ticket", cta_label=_t_text(i18n, lang, "email_support_cta_open_ticket"),
) )
@@ -809,23 +831,25 @@ def render_support_user_reply_admin(
snapshot_rows: Sequence[Tuple[str, str]], snapshot_rows: Sequence[Tuple[str, str]],
ticket_url: Optional[str], ticket_url: Optional[str],
) -> EmailContent: ) -> EmailContent:
i18n = _resolve_i18n(i18n)
lang = _normalize_lang(language, settings)
rows = [ rows = [
("Ticket", f"#{ticket_id}"), ("email_support_row_ticket", f"#{ticket_id}"),
("User", user_display), ("email_support_row_user", user_display),
("Subject", subject), ("email_support_row_subject", subject),
*snapshot_rows, *snapshot_rows,
] ]
return _support_email( return _support_email(
settings, settings,
i18n, i18n,
language, lang,
subject=f"New user reply in ticket #{ticket_id}", subject=_t_text(i18n, lang, "email_support_user_reply_admin_subject", ticket_id=ticket_id),
heading=f"User replied in ticket #{ticket_id}", heading=_t_text(i18n, lang, "email_support_user_reply_admin_heading", ticket_id=ticket_id),
intro="A user sent a new support message.", intro=_t_text(i18n, lang, "email_support_user_reply_admin_intro"),
rows=rows, rows=rows,
body_preview=body_preview, body_preview=body_preview,
ticket_url=ticket_url, ticket_url=ticket_url,
cta_label="Open ticket", cta_label=_t_text(i18n, lang, "email_support_cta_open_ticket"),
) )
@@ -839,17 +863,22 @@ def render_support_admin_reply_user(
body_preview: str, body_preview: str,
ticket_url: Optional[str], ticket_url: Optional[str],
) -> EmailContent: ) -> EmailContent:
i18n = _resolve_i18n(i18n)
lang = _normalize_lang(language, settings)
return _support_email( return _support_email(
settings, settings,
i18n, i18n,
language, lang,
subject=f"New reply for ticket #{ticket_id}", subject=_t_text(i18n, lang, "email_support_admin_reply_user_subject", ticket_id=ticket_id),
heading=f"New reply for ticket #{ticket_id}", heading=_t_text(i18n, lang, "email_support_admin_reply_user_heading", ticket_id=ticket_id),
intro="Support has replied to your ticket.", intro=_t_text(i18n, lang, "email_support_admin_reply_user_intro"),
rows=[("Ticket", f"#{ticket_id}"), ("Subject", subject)], rows=[
("email_support_row_ticket", f"#{ticket_id}"),
("email_support_row_subject", subject),
],
body_preview=body_preview, body_preview=body_preview,
ticket_url=ticket_url, ticket_url=ticket_url,
cta_label="Open in Mini App", cta_label=_t_text(i18n, lang, "email_support_cta_open_mini_app"),
) )
@@ -863,15 +892,24 @@ def render_support_ticket_closed_user(
body_preview: str = "", body_preview: str = "",
ticket_url: Optional[str], ticket_url: Optional[str],
) -> EmailContent: ) -> EmailContent:
i18n = _resolve_i18n(i18n)
lang = _normalize_lang(language, settings)
return _support_email( return _support_email(
settings, settings,
i18n, i18n,
language, lang,
subject=f"Ticket #{ticket_id} was closed", subject=_t_text(
heading=f"Ticket #{ticket_id} was closed", i18n, lang, "email_support_ticket_closed_user_subject", ticket_id=ticket_id
intro="Your support ticket has been closed.", ),
rows=[("Ticket", f"#{ticket_id}"), ("Subject", subject)], heading=_t_text(
body_preview=body_preview or "The ticket is closed.", i18n, lang, "email_support_ticket_closed_user_heading", ticket_id=ticket_id
),
intro=_t_text(i18n, lang, "email_support_ticket_closed_user_intro"),
rows=[
("email_support_row_ticket", f"#{ticket_id}"),
("email_support_row_subject", subject),
],
body_preview=body_preview or _t_text(i18n, lang, "email_support_ticket_closed_user_body"),
ticket_url=ticket_url, ticket_url=ticket_url,
cta_label="Open in Mini App", cta_label=_t_text(i18n, lang, "email_support_cta_open_mini_app"),
) )
+4 -4
View File
@@ -273,10 +273,10 @@ class NotificationService:
return [] return []
rows = [] rows = []
for key, label in ( for key, label in (
("tariff", "Tariff"), ("tariff", "email_support_row_tariff"),
("end_date", "End date"), ("end_date", "email_support_row_end_date"),
("remaining", "Remaining"), ("remaining", "email_support_row_remaining"),
("panel_status", "Panel status"), ("panel_status", "email_support_row_panel_status"),
): ):
value = snapshot.get(key) value = snapshot.get(key)
if value: if value:
+22
View File
@@ -664,6 +664,28 @@
"email_subscription_lifecycle_row_end_date": "Active until", "email_subscription_lifecycle_row_end_date": "Active until",
"email_subscription_lifecycle_cta": "Open dashboard", "email_subscription_lifecycle_cta": "Open dashboard",
"email_subscription_lifecycle_text_renew": "Dashboard: {url}", "email_subscription_lifecycle_text_renew": "Dashboard: {url}",
"email_support_new_ticket_admin_subject": "New support ticket #{ticket_id}",
"email_support_new_ticket_admin_heading": "New support ticket #{ticket_id}",
"email_support_new_ticket_admin_intro": "A user opened a new support ticket.",
"email_support_user_reply_admin_subject": "New user reply in ticket #{ticket_id}",
"email_support_user_reply_admin_heading": "User replied in ticket #{ticket_id}",
"email_support_user_reply_admin_intro": "A user sent a new support message.",
"email_support_admin_reply_user_subject": "New reply for ticket #{ticket_id}",
"email_support_admin_reply_user_heading": "New reply for ticket #{ticket_id}",
"email_support_admin_reply_user_intro": "Support has replied to your ticket.",
"email_support_ticket_closed_user_subject": "Ticket #{ticket_id} was closed",
"email_support_ticket_closed_user_heading": "Ticket #{ticket_id} was closed",
"email_support_ticket_closed_user_intro": "Your support ticket has been closed.",
"email_support_ticket_closed_user_body": "The ticket is closed.",
"email_support_row_ticket": "Ticket",
"email_support_row_user": "User",
"email_support_row_subject": "Subject",
"email_support_row_tariff": "Tariff",
"email_support_row_end_date": "End date",
"email_support_row_remaining": "Remaining",
"email_support_row_panel_status": "Panel status",
"email_support_cta_open_ticket": "Open ticket",
"email_support_cta_open_mini_app": "Open in Mini App",
"wa_loading": "Loading...", "wa_loading": "Loading...",
"wa_back": "Back", "wa_back": "Back",
"wa_next": "Next", "wa_next": "Next",
+22
View File
@@ -664,6 +664,28 @@
"email_subscription_lifecycle_row_end_date": "Действует до", "email_subscription_lifecycle_row_end_date": "Действует до",
"email_subscription_lifecycle_cta": "Открыть кабинет", "email_subscription_lifecycle_cta": "Открыть кабинет",
"email_subscription_lifecycle_text_renew": "Кабинет: {url}", "email_subscription_lifecycle_text_renew": "Кабинет: {url}",
"email_support_new_ticket_admin_subject": "Новый тикет поддержки #{ticket_id}",
"email_support_new_ticket_admin_heading": "Новый тикет поддержки #{ticket_id}",
"email_support_new_ticket_admin_intro": "Пользователь создал новый тикет поддержки.",
"email_support_user_reply_admin_subject": "Новый ответ пользователя в тикете #{ticket_id}",
"email_support_user_reply_admin_heading": "Пользователь ответил в тикете #{ticket_id}",
"email_support_user_reply_admin_intro": "Пользователь отправил новое сообщение в поддержку.",
"email_support_admin_reply_user_subject": "Новый ответ по тикету #{ticket_id}",
"email_support_admin_reply_user_heading": "Новый ответ по тикету #{ticket_id}",
"email_support_admin_reply_user_intro": "Поддержка ответила на ваш тикет.",
"email_support_ticket_closed_user_subject": "Тикет #{ticket_id} закрыт",
"email_support_ticket_closed_user_heading": "Тикет #{ticket_id} закрыт",
"email_support_ticket_closed_user_intro": "Ваш тикет поддержки закрыт.",
"email_support_ticket_closed_user_body": "Тикет закрыт.",
"email_support_row_ticket": "Тикет",
"email_support_row_user": "Пользователь",
"email_support_row_subject": "Тема",
"email_support_row_tariff": "Тариф",
"email_support_row_end_date": "Дата окончания",
"email_support_row_remaining": "Осталось",
"email_support_row_panel_status": "Статус в панели",
"email_support_cta_open_ticket": "Открыть тикет",
"email_support_cta_open_mini_app": "Открыть в Mini App",
"wa_loading": "Загрузка...", "wa_loading": "Загрузка...",
"wa_back": "Назад", "wa_back": "Назад",
"wa_next": "Далее", "wa_next": "Далее",
+259
View File
@@ -0,0 +1,259 @@
import json
import re
from pathlib import Path
from types import SimpleNamespace
from bot.middlewares.i18n import JsonI18n
from bot.services.email_templates import (
EmailContent,
render_account_merged,
render_login_code,
render_payment_success,
render_subscription_expiring,
render_subscription_lifecycle_notification,
render_support_admin_reply_user,
render_support_new_ticket_admin,
render_support_ticket_closed_user,
render_support_user_reply_admin,
render_user_notification,
)
REPO_ROOT = Path(__file__).resolve().parents[1]
EMAIL_KEY_RE = re.compile(r"""["'](?P<key>email_[a-z0-9_]+)["']""")
EMAIL_KEY_ASSIGNMENT_HINTS = (
"subject_key",
"heading_key",
"intro_key",
"cta_label_key",
)
def _settings(default_language: str = "ru"):
return SimpleNamespace(
DEFAULT_LANGUAGE=default_language,
EMAIL_CODE_TTL_SECONDS=600,
WEBAPP_LOGO_URL="",
WEBAPP_LOGO_USE_EMOJI=False,
WEBAPP_PRIMARY_COLOR="#00fe7a",
WEBAPP_TITLE="Mini Shop",
)
def _i18n(default_language: str = "ru"):
return JsonI18n(str(REPO_ROOT / "locales"), default=default_language)
def _locale_keys(language: str) -> set[str]:
return set(json.loads((REPO_ROOT / "locales" / f"{language}.json").read_text("utf-8")))
def _email_locale_keys_from(path: Path) -> set[str]:
text = path.read_text("utf-8")
keys: set[str] = set()
for match in EMAIL_KEY_RE.finditer(text):
line_start = text.rfind("\n", 0, match.start()) + 1
line_end = text.find("\n", match.end())
line = text[line_start : line_end if line_end != -1 else len(text)]
if "template_prefix" in line:
continue
if path.name == "email_templates.py" or any(
hint in line for hint in EMAIL_KEY_ASSIGNMENT_HINTS
):
keys.add(match.group("key"))
return keys
def _email_preview_locale_keys() -> set[str]:
path = REPO_ROOT / "docs-site" / "src" / "lib" / "emailPreviews.mjs"
return {match.group("key") for match in EMAIL_KEY_RE.finditer(path.read_text("utf-8"))}
def _used_email_locale_keys() -> set[str]:
keys = _email_preview_locale_keys()
for path in (REPO_ROOT / "backend" / "bot").rglob("*.py"):
keys.update(_email_locale_keys_from(path))
return keys
def _assert_content_is_localized(content: EmailContent, language: str):
assert content.subject
assert content.text
assert content.html
assert f'<html lang="{language}"' in content.html
assert "email_" not in content.subject
assert "email_" not in content.text
assert "email_" not in content.html
def _all_rendered_email_variants(language: str) -> list[EmailContent]:
settings = _settings(default_language=language)
i18n = _i18n(default_language=language)
dashboard_url = "https://app.example.com/account"
ticket_url = "https://app.example.com/support/42"
contents: list[EmailContent] = [
render_login_code(
settings,
code="123456",
language_code=language,
magic_link="https://app.example.com/magic",
purpose="login",
i18n=i18n,
),
render_login_code(
settings,
code="123456",
language_code=language,
purpose="set_password",
i18n=i18n,
),
render_account_merged(
settings,
language_code=language,
primary_user_id=100200300,
removed_user_id=-42,
final_end_date_text="2026-06-21 10:00",
i18n=i18n,
),
render_user_notification(
settings,
language_code=language,
subject=i18n.gettext(language, "email_payment_failed_subject"),
heading=i18n.gettext(language, "email_payment_failed_subject"),
intro=i18n.gettext(language, "email_user_notification_intro"),
cta_label=i18n.gettext(language, "email_user_notification_cta"),
message_text="Payment status changed.",
dashboard_url=dashboard_url,
i18n=i18n,
),
]
for sale_mode, months, traffic_gb in (
("subscription", 1, None),
("traffic", 0, 100),
("premium_topup", 0, 25),
("hwid_device", 2, None),
("tariff_upgrade", 0, None),
):
contents.append(
render_payment_success(
settings,
language_code=language,
sale_mode=sale_mode,
months=months,
traffic_gb=traffic_gb,
amount=390,
currency="RUB",
end_date_text="2026-06-21 10:00",
dashboard_url=dashboard_url,
provider_label="YooKassa",
i18n=i18n,
)
)
for days_left in (0, 1, 3):
contents.append(
render_subscription_expiring(
settings,
language_code=language,
days_left=days_left,
end_date_text="2026-06-21 10:00",
dashboard_url=dashboard_url,
i18n=i18n,
)
)
lifecycle_variants = (
{"notification_key": "before_2d_autorenew"},
{"notification_key": "expired"},
{"notification_key": "expired_24h_after"},
{"notification_key": "before_days", "days_left": 3},
{"notification_key": "before_hours", "hours_before": 6},
)
for kwargs in lifecycle_variants:
contents.append(
render_subscription_lifecycle_notification(
settings,
language_code=language,
message_text="Subscription lifecycle message.",
end_date_text="2026-06-21 10:00",
dashboard_url=dashboard_url,
i18n=i18n,
**kwargs,
)
)
contents.extend(
[
render_support_new_ticket_admin(
settings,
i18n,
language,
ticket_id=42,
user_display="user@example.com",
subject="Connection issue",
body_preview="Cannot connect.",
snapshot_rows=[
("email_support_row_tariff", "Premium"),
("email_support_row_remaining", "3 d. 4 h."),
],
ticket_url="https://app.example.com/admin/support/42",
),
render_support_user_reply_admin(
settings,
i18n,
language,
ticket_id=42,
user_display="user@example.com",
subject="Connection issue",
body_preview="Still cannot connect.",
snapshot_rows=[
("email_support_row_tariff", "Premium"),
("email_support_row_remaining", "3 d. 4 h."),
],
ticket_url="https://app.example.com/admin/support/42",
),
render_support_admin_reply_user(
settings,
i18n,
language,
ticket_id=42,
subject="Connection issue",
body_preview="Please try again.",
ticket_url=ticket_url,
),
render_support_ticket_closed_user(
settings,
i18n,
language,
ticket_id=42,
subject="Connection issue",
ticket_url=ticket_url,
),
]
)
return contents
def test_email_locale_keys_used_by_mailers_exist_in_base_locales():
used_keys = _used_email_locale_keys()
assert sorted(used_keys - _locale_keys("en")) == []
assert sorted(used_keys - _locale_keys("ru")) == []
def test_all_email_template_variants_render_without_raw_locale_keys():
for language in ("en", "ru"):
for content in _all_rendered_email_variants(language):
_assert_content_is_localized(content, language)
def test_support_email_templates_use_russian_copy_for_russian_recipients():
subjects = [content.subject for content in _all_rendered_email_variants("ru")[-4:]]
assert subjects == [
"Новый тикет поддержки #42",
"Новый ответ пользователя в тикете #42",
"Новый ответ по тикету #42",
"Тикет #42 закрыт",
]
+51
View File
@@ -1,9 +1,17 @@
import asyncio import asyncio
from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from bot.middlewares.i18n import JsonI18n
from bot.services.email_templates import (
render_support_new_ticket_admin,
render_support_ticket_closed_user,
)
from bot.services.notification_service import NotificationService from bot.services.notification_service import NotificationService
from config.settings import Settings from config.settings import Settings
REPO_ROOT = Path(__file__).resolve().parents[1]
def _settings(**overrides): def _settings(**overrides):
data = { data = {
@@ -15,6 +23,49 @@ def _settings(**overrides):
return Settings(_env_file=None, **data) return Settings(_env_file=None, **data)
def _i18n():
return JsonI18n(str(REPO_ROOT / "locales"), default="ru")
def test_support_ticket_closed_email_uses_user_language():
content = render_support_ticket_closed_user(
_settings(DEFAULT_LANGUAGE="en"),
_i18n(),
"ru",
ticket_id=7,
subject="Проблема с подключением",
ticket_url="https://app.example.com/support/7",
)
assert content.subject == "Тикет #7 закрыт"
assert '<html lang="ru"' in content.html
assert "Ticket #7 was closed" not in content.text
assert "Тема: Проблема с подключением" in content.text
assert "Открыть в Mini App" in content.html
def test_support_admin_email_localizes_snapshot_rows_for_recipient():
content = render_support_new_ticket_admin(
_settings(DEFAULT_LANGUAGE="en"),
_i18n(),
"ru",
ticket_id=11,
user_display="user@example.com",
subject="Не работает сайт",
body_preview="Не открывается личный кабинет",
snapshot_rows=[
("email_support_row_tariff", "Стандарт"),
("email_support_row_remaining", "3 д. 2 ч."),
],
ticket_url="https://app.example.com/admin/support/11",
)
assert content.subject == "Новый тикет поддержки #11"
assert "Тариф: Стандарт" in content.text
assert "Осталось: 3 д. 2 ч." in content.text
assert "Tariff: Standard" not in content.text
def test_support_ticket_url_uses_subscription_mini_app_url(): def test_support_ticket_url_uses_subscription_mini_app_url():
service = NotificationService( service = NotificationService(
bot=SimpleNamespace(), bot=SimpleNamespace(),