fix: improve subscription email renewal flow

This commit is contained in:
3252a8
2026-05-30 00:23:53 +03:00
parent 49781af921
commit 09be82aad5
14 changed files with 352 additions and 38 deletions
@@ -1,6 +1,7 @@
import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from urllib.parse import parse_qs, urlsplit
from aiogram.exceptions import TelegramBadRequest
from aiogram.methods import SendMessage
@@ -22,6 +23,8 @@ class FakeI18n:
"email_subscription_lifecycle_subject_expired_after": "Expired yesterday",
"email_subscription_lifecycle_subject_autorenew": "Auto-renewal tomorrow",
"email_subscription_lifecycle_intro": "Subscription notice",
"email_subscription_lifecycle_intro_direct": "Direct email notice",
"email_subscription_lifecycle_intro_mirrored": "Mirrored Telegram notice",
"email_subscription_lifecycle_row_end_date": "Active until",
"email_subscription_lifecycle_cta": "Open dashboard",
"email_subscription_lifecycle_text_renew": "Dashboard: {url}",
@@ -78,23 +81,29 @@ def _settings(**overrides):
)
def _subscription():
return SimpleNamespace(
subscription_id=42,
user_id=123,
end_date=datetime(2026, 6, 1, tzinfo=timezone.utc),
)
def _subscription(**overrides):
data = {
"subscription_id": 42,
"user_id": 123,
"end_date": datetime(2026, 6, 1, tzinfo=timezone.utc),
"tariff_key": "standard",
"provider": "yookassa",
"status_from_panel": "ACTIVE",
}
data.update(overrides)
return SimpleNamespace(**data)
def _user(**overrides):
return SimpleNamespace(
user_id=123,
telegram_id=555,
email="user@example.test",
language_code="ru",
first_name="Ada",
**overrides,
)
data = {
"user_id": 123,
"telegram_id": 555,
"email": "user@example.test",
"language_code": "ru",
"first_name": "Ada",
}
data.update(overrides)
return SimpleNamespace(**data)
def test_send_stage_records_telegram_and_email_channel_keys(monkeypatch):
@@ -143,6 +152,7 @@ def test_send_stage_records_telegram_and_email_channel_keys(monkeypatch):
}
]
assert email_service.messages[0]["email"] == "user@example.test"
assert "Mirrored Telegram notice" in email_service.messages[0]["content"].html
assert recorded == ["before_3d:telegram", "before_3d:email"]
@@ -232,3 +242,113 @@ def test_terminal_telegram_failure_is_recorded_to_avoid_retry_spam(monkeypatch):
assert delivery.email_sent is False
assert bot.calls[0][0] == 777
assert recorded == ["before_3d:telegram"]
def test_email_only_user_gets_email_name_direct_copy_and_renewal_login_link(monkeypatch):
recorded = []
async def fake_has(session, subscription_id, notification_key):
return notification_key in recorded
async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
recorded.append(notification_key)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
bot = FakeBot()
email_service = FakeEmailService()
service = SubscriptionLifecycleNotificationService(
_settings(),
bot,
FakeI18n(),
email_service=email_service,
)
user = _user(user_id=-8758169927032035, telegram_id=None, first_name=None)
async def run():
return await service.send_stage(
object(),
_subscription(user_id=user.user_id),
SubscriptionNotificationStage(
key="expired",
message_key="subscription_72h_notification",
days_left=0,
),
user=user,
telegram_markup="markup",
)
delivery = asyncio.run(run())
assert delivery.telegram_sent is False
assert delivery.email_sent is True
assert bot.messages == []
content = email_service.messages[0]["content"]
assert "Hi user@example.test, expires on 2026-06-01" in content.text
assert "User -8758169927032035" not in content.text
assert "Direct email notice" in content.html
assert "Mirrored Telegram notice" not in content.html
dashboard_line = next(
line for line in content.text.splitlines() if line.startswith("Dashboard: ")
)
url = dashboard_line.removeprefix("Dashboard: ")
parsed = urlsplit(url)
query = parse_qs(parsed.query)
assert parsed.scheme == "https"
assert parsed.netloc == "app.example.test"
assert query["login"] == ["email_code"]
assert query["login_email"] == ["user@example.test"]
assert query["after_login"] == ["renew"]
assert query["renew"] == ["1"]
assert query["renew_tariff"] == ["standard"]
assert recorded == ["expired:email"]
def test_trial_subscription_renewal_link_omits_tariff_for_default_fallback(monkeypatch):
recorded = []
async def fake_has(session, subscription_id, notification_key):
return notification_key in recorded
async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
recorded.append(notification_key)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
email_service = FakeEmailService()
service = SubscriptionLifecycleNotificationService(
_settings(),
FakeBot(),
FakeI18n(),
email_service=email_service,
)
user = _user(user_id=-1001, telegram_id=None, first_name=None)
async def run():
return await service.send_stage(
object(),
_subscription(
user_id=user.user_id,
tariff_key="trial-tariff",
provider="trial",
status_from_panel="TRIAL",
),
SubscriptionNotificationStage(
key="expired",
message_key="subscription_72h_notification",
days_left=0,
),
user=user,
)
asyncio.run(run())
content = email_service.messages[0]["content"]
dashboard_line = next(
line for line in content.text.splitlines() if line.startswith("Dashboard: ")
)
query = parse_qs(urlsplit(dashboard_line.removeprefix("Dashboard: ")).query)
assert "renew_tariff" not in query