feat: tune visual of web app admin panel, tune limit messages and etc

This commit is contained in:
3252a8
2026-05-12 15:42:17 +03:00
parent 8af59750c6
commit 475a1953d1
34 changed files with 2777 additions and 114 deletions
+200
View File
@@ -0,0 +1,200 @@
"""Unit tests for admin-driven traffic grants on SubscriptionService.
These tests cover the new ``admin_grant_topup`` and ``admin_grant_premium_topup``
methods that let admins credit GB to a user as if they had purchased a top-up.
The DAL and panel-service interactions are mocked so the tests stay hermetic.
"""
import json
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from bot.services.panel_api_service import PanelApiService
from bot.services.subscription_service import SubscriptionService
from config.settings import Settings
def _tariffs_config_payload(premium: bool = False) -> dict:
tariff = {
"key": "standard",
"names": {"ru": "Стандарт"},
"descriptions": {"ru": "Base"},
"squad_uuids": ["squad-1"],
"billing_model": "period",
"monthly_gb": 100,
"prices_rub": {"1": 150},
"prices_stars": {"1": 0},
"enabled_periods": [1],
"enabled": True,
}
if premium:
tariff["premium_squad_uuids"] = ["premium-squad"]
tariff["premium_monthly_gb"] = 25
return {"default_tariff": "standard", "tariffs": [tariff]}
def _make_settings(payload: dict, tmpdir: str) -> Settings:
config_path = Path(tmpdir) / "tariffs.json"
config_path.write_text(json.dumps(payload), encoding="utf-8")
return Settings(
_env_file=None,
BOT_TOKEN="token",
POSTGRES_USER="app_user",
POSTGRES_PASSWORD="app_password",
TARIFFS_CONFIG_PATH=str(config_path),
)
class AdminGrantTopupTests(unittest.IsolatedAsyncioTestCase):
async def test_regular_grant_increases_balance_and_panel_limit(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(_tariffs_config_payload(), tmpdir)
panel_service = AsyncMock(spec=PanelApiService)
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
service = SubscriptionService(settings, panel_service)
db_user = SimpleNamespace(
user_id=42,
first_name="Tester",
last_name=None,
username="tester",
language_code="ru",
panel_user_uuid="panel-uuid",
email=None,
telegram_id=42,
)
sub = SimpleNamespace(
subscription_id=7,
user_id=42,
panel_user_uuid="panel-uuid",
end_date=datetime.now(timezone.utc) + timedelta(days=10),
tariff_key="standard",
tier_baseline_bytes=100 * (1024**3),
topup_balance_bytes=5 * (1024**3),
traffic_limit_bytes=105 * (1024**3),
is_throttled=True,
hwid_device_limit=3,
extra_hwid_devices=0,
premium_is_limited=False,
)
updated_sub = SimpleNamespace(**vars(sub))
updated_sub.topup_balance_bytes = 55 * (1024**3)
updated_sub.traffic_limit_bytes = 155 * (1024**3)
updated_sub.is_throttled = False
with patch("bot.services.subscription_service.user_dal.get_user_by_id", new=AsyncMock(return_value=db_user)), \
patch("bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id", new=AsyncMock(return_value=sub)), \
patch("bot.services.subscription_service.subscription_dal.update_subscription", new=AsyncMock(return_value=updated_sub)) as upd, \
patch("bot.services.subscription_service.tariff_dal.create_traffic_topup", new=AsyncMock()) as topup_log:
result = await service.admin_grant_topup(AsyncMock(), 42, 50.0)
self.assertIsNotNone(result)
self.assertEqual(result["topup_balance_bytes"], 55 * (1024**3))
self.assertEqual(result["traffic_limit_bytes"], 155 * (1024**3))
self.assertEqual(result["granted_bytes"], 50 * (1024**3))
upd.assert_awaited_once()
sub_update_payload = upd.await_args.args[2]
self.assertEqual(sub_update_payload["topup_balance_bytes"], 55 * (1024**3))
self.assertEqual(sub_update_payload["traffic_limit_bytes"], 155 * (1024**3))
self.assertFalse(sub_update_payload["is_throttled"])
topup_log.assert_awaited_once()
self.assertEqual(topup_log.await_args.kwargs["kind"], "admin_topup")
self.assertIsNone(topup_log.await_args.kwargs["payment_id"])
self.assertEqual(topup_log.await_args.kwargs["purchased_bytes"], 50 * (1024**3))
panel_service.update_user_details_on_panel.assert_awaited_once()
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
self.assertEqual(panel_payload["trafficLimitBytes"], 155 * (1024**3))
async def test_regular_grant_rejects_zero(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(_tariffs_config_payload(), tmpdir)
service = SubscriptionService(settings, AsyncMock(spec=PanelApiService))
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, 0))
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, -10))
async def test_premium_grant_clears_limited_state_when_balance_covers_overuse(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(_tariffs_config_payload(premium=True), tmpdir)
panel_service = AsyncMock(spec=PanelApiService)
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
service = SubscriptionService(settings, panel_service)
db_user = SimpleNamespace(
user_id=77,
first_name="Premium",
last_name=None,
username="premium",
language_code="ru",
panel_user_uuid="panel-uuid",
email=None,
telegram_id=77,
)
# User has used 30 GB on a 25 GB premium baseline → currently
# premium_is_limited=True. Admin grants 20 GB of premium top-up.
sub = SimpleNamespace(
subscription_id=9,
user_id=77,
panel_user_uuid="panel-uuid",
tariff_key="standard",
premium_baseline_bytes=25 * (1024**3),
premium_topup_balance_bytes=0,
premium_topup_used_bytes=0,
premium_used_bytes=30 * (1024**3),
premium_is_limited=True,
premium_period_start_at=datetime.now(timezone.utc).replace(
day=1, hour=0, minute=0, second=0, microsecond=0
),
premium_unlimited_override=False,
premium_bonus_bytes=0,
)
updated_sub = SimpleNamespace(**vars(sub))
with patch("bot.services.subscription_service.user_dal.get_user_by_id", new=AsyncMock(return_value=db_user)), \
patch("bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id", new=AsyncMock(return_value=sub)), \
patch("bot.services.subscription_service.subscription_dal.update_subscription", new=AsyncMock(return_value=updated_sub)) as upd, \
patch("bot.services.subscription_service.tariff_dal.create_traffic_topup", new=AsyncMock()) as topup_log:
result = await service.admin_grant_premium_topup(AsyncMock(), 77, 20.0)
self.assertIsNotNone(result)
self.assertFalse(result["premium_is_limited"])
# 5 GB of overuse should be backfilled into premium_topup_used.
self.assertEqual(result["premium_topup_used_bytes"], 5 * (1024**3))
# Remaining balance after backfill: 15 GB.
self.assertEqual(result["premium_topup_balance_bytes"], 15 * (1024**3))
self.assertEqual(result["granted_bytes"], 20 * (1024**3))
topup_log.assert_awaited_once()
self.assertEqual(topup_log.await_args.kwargs["kind"], "admin_premium_topup")
self.assertIsNone(topup_log.await_args.kwargs["payment_id"])
panel_service.update_user_details_on_panel.assert_awaited_once()
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
self.assertIn("premium-squad", panel_payload["activeInternalSquads"])
sub_update_payload = upd.await_args.args[2]
self.assertFalse(sub_update_payload["premium_is_limited"])
async def test_premium_grant_fails_when_tariff_has_no_premium_squads(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(_tariffs_config_payload(premium=False), tmpdir)
service = SubscriptionService(settings, AsyncMock(spec=PanelApiService))
db_user = SimpleNamespace(
user_id=11, panel_user_uuid="panel-uuid",
)
sub = SimpleNamespace(subscription_id=1, user_id=11, panel_user_uuid="panel-uuid", tariff_key="standard")
with patch("bot.services.subscription_service.user_dal.get_user_by_id", new=AsyncMock(return_value=db_user)), \
patch("bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id", new=AsyncMock(return_value=sub)):
self.assertIsNone(await service.admin_grant_premium_topup(AsyncMock(), 11, 10.0))
if __name__ == "__main__":
unittest.main()
+35
View File
@@ -0,0 +1,35 @@
import unittest
from bot.utils.mini_app_url import append_query_params, subscription_mini_app_topup_url
from config.settings import Settings
class MiniAppUrlTests(unittest.TestCase):
def test_append_query_params_adds_topup(self):
self.assertEqual(
append_query_params("https://app.example.com/home", {"topup": "regular"}),
"https://app.example.com/home?topup=regular",
)
def test_append_query_params_merges_existing(self):
self.assertEqual(
append_query_params("https://app.example.com/?lang=ru", {"topup": "premium"}),
"https://app.example.com/?lang=ru&topup=premium",
)
def test_subscription_mini_app_topup_url_none_when_unset(self):
s = Settings(_env_file=None, BOT_TOKEN="x", POSTGRES_USER="u", POSTGRES_PASSWORD="p", SUBSCRIPTION_MINI_APP_URL=None)
self.assertIsNone(subscription_mini_app_topup_url(s, "regular"))
def test_subscription_mini_app_topup_url(self):
s = Settings(
_env_file=None,
BOT_TOKEN="x",
POSTGRES_USER="u",
POSTGRES_PASSWORD="p",
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp",
)
self.assertEqual(
subscription_mini_app_topup_url(s, "regular"),
"https://app.example.com/webapp?topup=regular",
)
+118
View File
@@ -265,3 +265,121 @@ class TariffWorkerTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(sub.premium_topup_balance_bytes, int(1.5 * (1024**3)))
self.assertEqual(sub.premium_topup_used_bytes, 0)
self.assertEqual(sub.premium_period_start_at, datetime(2026, 6, 1, tzinfo=timezone.utc))
async def test_premium_unlimited_override_never_throttles(self):
payload = _tariffs_config_payload()
payload["tariffs"][0]["premium_squad_uuids"] = ["premium-squad"]
payload["tariffs"][0]["premium_monthly_gb"] = 1
with tempfile.TemporaryDirectory() as tmpdir:
config_path = Path(tmpdir) / "tariffs.json"
config_path.write_text(json.dumps(payload), encoding="utf-8")
settings = Settings(
_env_file=None,
BOT_TOKEN="token",
POSTGRES_USER="app_user",
POSTGRES_PASSWORD="app_password",
TARIFFS_CONFIG_PATH=str(config_path),
TARIFF_TRAFFIC_WARNING_LEVELS="101",
)
panel_service = AsyncMock(spec=PanelApiService)
panel_service.get_internal_squad_accessible_nodes = AsyncMock(
return_value=[{"uuid": "node-1"}]
)
panel_service.get_node_users_bandwidth_stats = AsyncMock(
return_value={
"topUsers": [
{"username": "tg_42", "total": 50 * (1024**3)},
]
}
)
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
subscription_service = SubscriptionService(settings, panel_service)
worker = TariffTrafficWorker(
settings=settings,
session_factory=SimpleNamespace(),
panel_service=panel_service,
subscription_service=subscription_service,
)
sub = SimpleNamespace(
subscription_id=1,
user_id=42,
panel_user_uuid="panel-uuid",
premium_baseline_bytes=1 * (1024**3),
premium_topup_balance_bytes=0,
premium_topup_used_bytes=0,
premium_used_bytes=0,
premium_is_limited=False,
premium_period_start_at=None,
premium_unlimited_override=True,
premium_bonus_bytes=0,
)
tariff = settings.tariffs_config.require("standard")
await worker._sync_premium_squad_limit(
AsyncMock(), sub, tariff, datetime.now(timezone.utc), panel_username="tg_42"
)
self.assertFalse(sub.premium_is_limited)
self.assertEqual(int(sub.premium_used_bytes), 50 * (1024**3))
payload_sent = panel_service.update_user_details_on_panel.await_args.args[1]
self.assertIn("premium-squad", payload_sent["activeInternalSquads"])
async def test_premium_bonus_extends_limit(self):
payload = _tariffs_config_payload()
payload["tariffs"][0]["premium_squad_uuids"] = ["premium-squad"]
payload["tariffs"][0]["premium_monthly_gb"] = 1
with tempfile.TemporaryDirectory() as tmpdir:
config_path = Path(tmpdir) / "tariffs.json"
config_path.write_text(json.dumps(payload), encoding="utf-8")
settings = Settings(
_env_file=None,
BOT_TOKEN="token",
POSTGRES_USER="app_user",
POSTGRES_PASSWORD="app_password",
TARIFFS_CONFIG_PATH=str(config_path),
TARIFF_TRAFFIC_WARNING_LEVELS="101",
)
panel_service = AsyncMock(spec=PanelApiService)
panel_service.get_internal_squad_accessible_nodes = AsyncMock(
return_value=[{"uuid": "node-1"}]
)
# Used 4 GB > tariff baseline 1 GB, but admin granted +10 GB bonus.
panel_service.get_node_users_bandwidth_stats = AsyncMock(
return_value={
"topUsers": [
{"username": "tg_77", "total": 4 * (1024**3)},
]
}
)
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
subscription_service = SubscriptionService(settings, panel_service)
worker = TariffTrafficWorker(
settings=settings,
session_factory=SimpleNamespace(),
panel_service=panel_service,
subscription_service=subscription_service,
)
sub = SimpleNamespace(
subscription_id=1,
user_id=77,
panel_user_uuid="panel-uuid",
premium_baseline_bytes=1 * (1024**3),
premium_topup_balance_bytes=0,
premium_topup_used_bytes=0,
premium_used_bytes=0,
premium_is_limited=False,
premium_period_start_at=None,
premium_unlimited_override=False,
premium_bonus_bytes=10 * (1024**3),
)
tariff = settings.tariffs_config.require("standard")
await worker._sync_premium_squad_limit(
AsyncMock(), sub, tariff, datetime.now(timezone.utc), panel_username="tg_77"
)
# 4 GB used vs 1 GB baseline + 10 GB bonus = 11 GB limit → not limited.
self.assertFalse(sub.premium_is_limited)
self.assertEqual(int(sub.premium_used_bytes), 4 * (1024**3))