fix: improve subscription email renewal flow
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -88,9 +88,7 @@ class TariffWorkerTests(unittest.IsolatedAsyncioTestCase):
|
||||
super().__init__("wrapped")
|
||||
self.orig = orig
|
||||
|
||||
self.assertTrue(
|
||||
TariffTrafficWorker._is_retryable_db_exception(WrappedDbError(PgError()))
|
||||
)
|
||||
self.assertTrue(TariffTrafficWorker._is_retryable_db_exception(WrappedDbError(PgError())))
|
||||
self.assertFalse(TariffTrafficWorker._is_retryable_db_exception(RuntimeError("plain")))
|
||||
|
||||
async def test_period_tariff_uses_panel_month_strategy_without_resetting(self):
|
||||
|
||||
@@ -76,10 +76,12 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual([plan["tariff_key"] for plan in plans], ["standard", "traffic"])
|
||||
self.assertEqual(plans[0]["sale_mode"], "subscription")
|
||||
self.assertTrue(plans[0]["is_default_tariff"])
|
||||
self.assertEqual(plans[0]["months"], 1)
|
||||
self.assertEqual(plans[0]["hwid_device_limit"], 5)
|
||||
self.assertEqual(plans[0]["hwid_device_packages"][0]["device_count"], 1)
|
||||
self.assertEqual(plans[1]["sale_mode"], "traffic_package")
|
||||
self.assertFalse(plans[1]["is_default_tariff"])
|
||||
self.assertEqual(plans[1]["traffic_gb"], 50.0)
|
||||
self.assertEqual(plans[1]["stars_price"], 2500)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user