fix: configure referral bonuses per tariff period
This commit is contained in:
@@ -66,6 +66,8 @@ ADMIN_TARIFF_SETTINGS_PAGE_KEYS = {
|
||||
"admin_tariffs_legacy_subtitle",
|
||||
"admin_tariffs_legacy_period",
|
||||
"admin_tariffs_legacy_enabled",
|
||||
"admin_tariffs_legacy_ref_inviter",
|
||||
"admin_tariffs_legacy_ref_referee",
|
||||
"admin_tariffs_legacy_traffic_packages",
|
||||
"admin_tariffs_legacy_stars_traffic_packages",
|
||||
"admin_tariffs_legacy_traffic_hint",
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import Any, Optional
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.services.referral_service import ReferralService
|
||||
from config.tariffs_config import TariffsConfig
|
||||
|
||||
|
||||
def _make_settings(**overrides: Any) -> SimpleNamespace:
|
||||
@@ -338,6 +339,87 @@ class RefereeBonusTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertFalse(result["inviter_bonus_applied_flag"])
|
||||
subscription_service.extend_active_subscription_days.assert_not_called()
|
||||
|
||||
async def test_tariff_bonus_uses_referee_purchase_tariff(self):
|
||||
data = {
|
||||
"default_tariff": "standard",
|
||||
"tariffs": [
|
||||
{
|
||||
"key": "standard",
|
||||
"names": {"en": "Standard"},
|
||||
"descriptions": {},
|
||||
"squad_uuids": ["standard-squad"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 100,
|
||||
"prices_rub": {"2": 400},
|
||||
"prices_stars": {},
|
||||
"enabled_periods": [2],
|
||||
"referral_bonus_days_inviter": {"2": 5},
|
||||
"referral_bonus_days_referee": {"2": 1},
|
||||
"enabled": True,
|
||||
},
|
||||
{
|
||||
"key": "premium",
|
||||
"names": {"en": "Premium"},
|
||||
"descriptions": {},
|
||||
"squad_uuids": ["premium-squad"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 500,
|
||||
"prices_rub": {"2": 700},
|
||||
"prices_stars": {},
|
||||
"enabled_periods": [2],
|
||||
"referral_bonus_days_inviter": {"2": 20},
|
||||
"referral_bonus_days_referee": {"2": 7},
|
||||
"enabled": True,
|
||||
},
|
||||
],
|
||||
}
|
||||
settings = _make_settings(
|
||||
REFERRAL_ONE_BONUS_PER_REFEREE=False,
|
||||
tariffs_config=TariffsConfig.model_validate(data),
|
||||
)
|
||||
subscription_service = AsyncMock()
|
||||
subscription_service.has_active_subscription = AsyncMock(return_value=False)
|
||||
subscription_service._get_or_create_panel_user_link_details = AsyncMock(
|
||||
return_value=("inviter-panel", "inviter-sub", "short", False)
|
||||
)
|
||||
inviter_new_end = datetime(2026, 4, 1, tzinfo=timezone.utc)
|
||||
referee_new_end = datetime(2026, 5, 1, tzinfo=timezone.utc)
|
||||
subscription_service.extend_active_subscription_days = AsyncMock(
|
||||
side_effect=[inviter_new_end, referee_new_end]
|
||||
)
|
||||
service, _bot = _make_service(settings=settings, subscription_service=subscription_service)
|
||||
|
||||
with patch(
|
||||
"bot.services.referral_service.user_dal.get_user_by_id",
|
||||
AsyncMock(
|
||||
side_effect=lambda session, uid: (
|
||||
_make_user(uid, referred_by_id=1) if uid == 42 else _make_user(uid)
|
||||
)
|
||||
),
|
||||
):
|
||||
result = await service.apply_referral_bonuses_for_payment(
|
||||
session=AsyncMock(),
|
||||
referee_user_id=42,
|
||||
purchased_subscription_months=2,
|
||||
skip_if_active_before_payment=False,
|
||||
tariff_key="premium",
|
||||
)
|
||||
|
||||
self.assertEqual(result["referee_bonus_applied_days"], 7)
|
||||
self.assertTrue(result["inviter_bonus_applied_flag"])
|
||||
inviter_call = [
|
||||
call
|
||||
for call in subscription_service.extend_active_subscription_days.await_args_list
|
||||
if call.kwargs.get("user_id") == 1
|
||||
][0]
|
||||
referee_call = [
|
||||
call
|
||||
for call in subscription_service.extend_active_subscription_days.await_args_list
|
||||
if call.kwargs.get("user_id") == 42
|
||||
][0]
|
||||
self.assertEqual(inviter_call.kwargs["bonus_days"], 20)
|
||||
self.assertEqual(referee_call.kwargs["bonus_days"], 7)
|
||||
|
||||
|
||||
class GenerateReferralLinkTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_includes_bot_username_and_referral_code(self):
|
||||
|
||||
@@ -42,6 +42,20 @@ class SettingsTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(settings.WEBAPP_TITLE, "/minishop")
|
||||
|
||||
def test_legacy_subscription_prices_have_defaults(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH="missing-tariffs.json",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
settings.subscription_options,
|
||||
{1: 200.0, 3: 600.0, 6: 1200.0, 12: 2400.0},
|
||||
)
|
||||
|
||||
def test_subscription_guides_defaults_are_enabled(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
|
||||
@@ -393,6 +393,65 @@ class SubscriptionServiceActivationDispatchTests(unittest.IsolatedAsyncioTestCas
|
||||
self.assertEqual(kwargs["payment_db_id"], 12)
|
||||
|
||||
|
||||
class SubscriptionServiceBonusExtensionTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_referral_extension_preserves_existing_tariff_limit(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(
|
||||
_tariffs_config_payload(),
|
||||
tmpdir,
|
||||
USER_TRAFFIC_LIMIT_GB=999,
|
||||
)
|
||||
service = _make_service(settings)
|
||||
service._get_or_create_panel_user_link_details = AsyncMock(
|
||||
return_value=("panel-user", "short-uuid", "short", False)
|
||||
)
|
||||
service.panel_service.update_user_details_on_panel = AsyncMock(
|
||||
return_value={"ok": True}
|
||||
)
|
||||
active_sub = SimpleNamespace(
|
||||
subscription_id=10,
|
||||
end_date=datetime.now(timezone.utc) + timedelta(days=5),
|
||||
traffic_limit_bytes=100 * GIB,
|
||||
tariff_key="standard",
|
||||
)
|
||||
updated_sub = SimpleNamespace(
|
||||
subscription_id=10,
|
||||
end_date=active_sub.end_date + timedelta(days=3),
|
||||
traffic_limit_bytes=100 * GIB,
|
||||
tariff_key="standard",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.user_dal.get_user_by_id",
|
||||
AsyncMock(return_value=SimpleNamespace(user_id=42)),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.subscription_dal.get_active_subscription_by_user_id",
|
||||
AsyncMock(return_value=active_sub),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.subscription_dal.update_subscription_end_date",
|
||||
AsyncMock(return_value=updated_sub),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.lifecycle.subscription_dal.update_subscription",
|
||||
AsyncMock(),
|
||||
) as update_subscription,
|
||||
):
|
||||
await service.extend_active_subscription_days(
|
||||
session=AsyncMock(),
|
||||
user_id=42,
|
||||
bonus_days=3,
|
||||
reason="referral bonus from Alice",
|
||||
)
|
||||
|
||||
update_subscription.assert_not_awaited()
|
||||
payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertNotIn("trafficLimitBytes", payload)
|
||||
self.assertNotIn("trafficLimitStrategy", payload)
|
||||
|
||||
|
||||
class SubscriptionServiceActiveDetailsTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _local_active_sub(self) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
|
||||
@@ -70,6 +70,28 @@ class TariffsConfigTests(unittest.TestCase):
|
||||
self.assertIsNotNone(packages)
|
||||
self.assertEqual(packages.rub[0].gb, 25)
|
||||
|
||||
def test_period_tariff_referral_bonuses_load(self):
|
||||
data = _valid_config()
|
||||
data["tariffs"][0]["referral_bonus_days_inviter"] = {"2": 5, "4": 10}
|
||||
data["tariffs"][0]["referral_bonus_days_referee"] = {"2": 1, "4": 2}
|
||||
data["tariffs"][0]["prices_rub"] = {"2": 400, "4": 800}
|
||||
data["tariffs"][0]["prices_stars"] = {}
|
||||
data["tariffs"][0]["enabled_periods"] = [2, 4]
|
||||
|
||||
config = TariffsConfig.model_validate(data)
|
||||
tariff = config.require("standard")
|
||||
|
||||
self.assertEqual(tariff.referral_inviter_bonus_days(2), 5)
|
||||
self.assertEqual(tariff.referral_referee_bonus_days(4), 2)
|
||||
self.assertIsNone(tariff.referral_inviter_bonus_days(8))
|
||||
|
||||
def test_negative_tariff_referral_bonus_rejected(self):
|
||||
data = _valid_config()
|
||||
data["tariffs"][0]["referral_bonus_days_inviter"] = {"1": -1}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
TariffsConfig.model_validate(data)
|
||||
|
||||
def test_missing_config_returns_none(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -83,6 +83,98 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(plans[1]["traffic_gb"], 50.0)
|
||||
self.assertEqual(plans[1]["stars_price"], 2500)
|
||||
|
||||
def test_referral_bonus_details_use_custom_tariff_periods(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir) / "tariffs.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_tariff": "standard",
|
||||
"tariffs": [
|
||||
{
|
||||
"key": "standard",
|
||||
"names": {"en": "Standard"},
|
||||
"descriptions": {"en": "Custom periods"},
|
||||
"squad_uuids": ["uuid"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 100,
|
||||
"prices_rub": {
|
||||
"2": 400,
|
||||
"4": 800,
|
||||
"8": 1600,
|
||||
"16": 3200,
|
||||
},
|
||||
"prices_stars": {},
|
||||
"referral_bonus_days_inviter": {
|
||||
"2": 5,
|
||||
"4": 10,
|
||||
"16": 40,
|
||||
},
|
||||
"referral_bonus_days_referee": {
|
||||
"2": 1,
|
||||
"4": 2,
|
||||
"8": 4,
|
||||
},
|
||||
"enabled_periods": [2, 4, 8, 16],
|
||||
"enabled": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH=str(path),
|
||||
)
|
||||
|
||||
details = subscription_webapp._serialize_referral_bonus_details(settings, "en")
|
||||
|
||||
self.assertEqual(
|
||||
details,
|
||||
[
|
||||
{
|
||||
"id": "standard:2",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 2,
|
||||
"title": "Standard - 2 months",
|
||||
"inviter_days": 5,
|
||||
"friend_days": 1,
|
||||
},
|
||||
{
|
||||
"id": "standard:4",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 4,
|
||||
"title": "Standard - 4 months",
|
||||
"inviter_days": 10,
|
||||
"friend_days": 2,
|
||||
},
|
||||
{
|
||||
"id": "standard:8",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 8,
|
||||
"title": "Standard - 8 months",
|
||||
"inviter_days": 0,
|
||||
"friend_days": 4,
|
||||
},
|
||||
{
|
||||
"id": "standard:16",
|
||||
"tariff_key": "standard",
|
||||
"tariff_name": "Standard",
|
||||
"months": 16,
|
||||
"title": "Standard - 16 months",
|
||||
"inviter_days": 40,
|
||||
"friend_days": 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_subscription_template_does_not_block_on_telegram_sdk(self):
|
||||
html = subscription_webapp.TEMPLATE_PATH.read_text(encoding="utf-8")
|
||||
|
||||
@@ -653,6 +745,9 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
YOOKASSA_ENABLED=False,
|
||||
CRYPTOPAY_ENABLED=False,
|
||||
TARIFFS_CONFIG_PATH="missing-tariffs.json",
|
||||
MONTH_3_ENABLED=False,
|
||||
MONTH_6_ENABLED=False,
|
||||
MONTH_12_ENABLED=False,
|
||||
RUB_PRICE_1_MONTH=None,
|
||||
STARS_PRICE_1_MONTH=250,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user