refactor: remove modem functionality from classic subscriptions

Remove all modem purchase/management code:
- Delete modem handler, service, and tests
- Remove modem button from keyboards and admin panel
- Remove modem pricing from calculations
- Remove modem REST API endpoint and schemas
- Remove modem decorator, config settings, and notification formatting
- Keep DB column and migration for backwards compatibility
This commit is contained in:
Fringg
2026-02-11 21:14:08 +03:00
parent d05ff678ab
commit ee2e79db31
18 changed files with 4 additions and 1406 deletions

View File

@@ -1,395 +0,0 @@
"""
Тесты для ModemService - управление модемом в подписке.
"""
from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from app.services.modem_service import (
ModemError,
ModemService,
get_modem_service,
)
def create_mock_settings():
"""Создаёт мок настроек приложения."""
settings = MagicMock()
settings.is_modem_enabled.return_value = True
settings.get_modem_price_per_month.return_value = 10000 # 100 рублей
settings.get_modem_period_discount.return_value = 0
return settings
def create_sample_user():
"""Создаёт пример пользователя."""
user = SimpleNamespace(
id=1,
telegram_id=123456789,
balance_kopeks=50000, # 500 рублей
language='ru',
subscription=None,
)
return user
def create_sample_subscription():
"""Создаёт пример подписки."""
subscription = SimpleNamespace(
id=1,
user_id=1,
is_trial=False,
modem_enabled=False,
device_limit=2,
end_date=datetime.utcnow() + timedelta(days=30),
updated_at=datetime.utcnow(),
)
return subscription
def create_trial_subscription():
"""Создаёт триальную подписку."""
subscription = SimpleNamespace(
id=2,
user_id=1,
is_trial=True,
modem_enabled=False,
device_limit=1,
end_date=datetime.utcnow() + timedelta(days=7),
updated_at=datetime.utcnow(),
)
return subscription
def create_modem_service(monkeypatch):
"""Создаёт ModemService с замоканными настройками."""
mock_settings = create_mock_settings()
monkeypatch.setattr('app.services.modem_service.settings', mock_settings)
return ModemService(), mock_settings
class TestModemServiceAvailability:
"""Тесты проверки доступности модема."""
def test_check_availability_no_subscription(self, monkeypatch):
"""Модем недоступен без подписки."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_user.subscription = None
result = modem_service.check_availability(sample_user)
assert not result.available
assert result.error == ModemError.NO_SUBSCRIPTION
assert not result.modem_enabled
def test_check_availability_trial_subscription(self, monkeypatch):
"""Модем недоступен для триальной подписки."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
trial_subscription = create_trial_subscription()
sample_user.subscription = trial_subscription
result = modem_service.check_availability(sample_user)
assert not result.available
assert result.error == ModemError.TRIAL_SUBSCRIPTION
assert not result.modem_enabled
def test_check_availability_modem_disabled_in_settings(self, monkeypatch):
"""Модем недоступен, если отключён в настройках."""
modem_service, mock_settings = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_user.subscription = sample_subscription
mock_settings.is_modem_enabled.return_value = False
result = modem_service.check_availability(sample_user)
assert not result.available
assert result.error == ModemError.MODEM_DISABLED
def test_check_availability_success(self, monkeypatch):
"""Модем доступен для платной подписки."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_user.subscription = sample_subscription
result = modem_service.check_availability(sample_user)
assert result.available
assert result.error is None
assert not result.modem_enabled
def test_check_availability_for_enable_already_enabled(self, monkeypatch):
"""Нельзя подключить уже подключенный модем."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_subscription.modem_enabled = True
sample_user.subscription = sample_subscription
result = modem_service.check_availability(sample_user, for_enable=True)
assert not result.available
assert result.error == ModemError.ALREADY_ENABLED
assert result.modem_enabled
def test_check_availability_for_disable_not_enabled(self, monkeypatch):
"""Нельзя отключить неподключенный модем."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_subscription.modem_enabled = False
sample_user.subscription = sample_subscription
result = modem_service.check_availability(sample_user, for_disable=True)
assert not result.available
assert result.error == ModemError.NOT_ENABLED
assert not result.modem_enabled
class TestModemServicePricing:
"""Тесты расчёта цены модема."""
def test_calculate_price_one_month(self, monkeypatch):
"""Расчёт цены на 1 месяц."""
modem_service, _ = create_modem_service(monkeypatch)
sample_subscription = create_sample_subscription()
sample_subscription.end_date = datetime.utcnow() + timedelta(days=30)
result = modem_service.calculate_price(sample_subscription)
assert result.base_price == 10000
assert result.final_price == 10000
assert result.charged_months == 1
assert result.discount_percent == 0
assert not result.has_discount
def test_calculate_price_three_months(self, monkeypatch):
"""Расчёт цены на 3 месяца."""
modem_service, _ = create_modem_service(monkeypatch)
sample_subscription = create_sample_subscription()
sample_subscription.end_date = datetime.utcnow() + timedelta(days=90)
result = modem_service.calculate_price(sample_subscription)
assert result.base_price == 30000 # 3 * 10000
assert result.charged_months == 3
def test_calculate_price_with_discount(self, monkeypatch):
"""Расчёт цены со скидкой."""
modem_service, mock_settings = create_modem_service(monkeypatch)
sample_subscription = create_sample_subscription()
sample_subscription.end_date = datetime.utcnow() + timedelta(days=90)
mock_settings.get_modem_period_discount.return_value = 10 # 10% скидка
result = modem_service.calculate_price(sample_subscription)
assert result.base_price == 30000
assert result.discount_percent == 10
assert result.discount_amount == 3000
assert result.final_price == 27000
assert result.has_discount
class TestModemServiceBalance:
"""Тесты проверки баланса."""
def test_check_balance_sufficient(self, monkeypatch):
"""Баланса достаточно."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_user.balance_kopeks = 50000
has_funds, missing = modem_service.check_balance(sample_user, 10000)
assert has_funds
assert missing == 0
def test_check_balance_insufficient(self, monkeypatch):
"""Баланса недостаточно."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_user.balance_kopeks = 5000
has_funds, missing = modem_service.check_balance(sample_user, 10000)
assert not has_funds
assert missing == 5000
def test_check_balance_zero_price(self, monkeypatch):
"""Нулевая цена - всегда достаточно."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_user.balance_kopeks = 0
has_funds, missing = modem_service.check_balance(sample_user, 0)
assert has_funds
assert missing == 0
class TestModemServicePeriodWarning:
"""Тесты предупреждений о сроке действия."""
def test_warning_critical(self, monkeypatch):
"""Критическое предупреждение при <= 7 днях."""
modem_service, _ = create_modem_service(monkeypatch)
assert modem_service.get_period_warning_level(7) == 'critical'
assert modem_service.get_period_warning_level(5) == 'critical'
assert modem_service.get_period_warning_level(1) == 'critical'
def test_warning_info(self, monkeypatch):
"""Информационное предупреждение при <= 30 днях."""
modem_service, _ = create_modem_service(monkeypatch)
assert modem_service.get_period_warning_level(30) == 'info'
assert modem_service.get_period_warning_level(15) == 'info'
assert modem_service.get_period_warning_level(8) == 'info'
def test_warning_none(self, monkeypatch):
"""Нет предупреждения при > 30 днях."""
modem_service, _ = create_modem_service(monkeypatch)
assert modem_service.get_period_warning_level(31) is None
assert modem_service.get_period_warning_level(60) is None
assert modem_service.get_period_warning_level(90) is None
class TestModemServiceEnable:
"""Тесты подключения модема."""
async def test_enable_modem_success(self, monkeypatch):
"""Успешное подключение модема."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_user.subscription = sample_subscription
sample_user.balance_kopeks = 50000
mock_db = AsyncMock()
mock_subtract = AsyncMock(return_value=True)
mock_create_transaction = AsyncMock()
mock_update_remnawave = AsyncMock()
monkeypatch.setattr('app.services.modem_service.subtract_user_balance', mock_subtract)
monkeypatch.setattr('app.services.modem_service.create_transaction', mock_create_transaction)
modem_service._subscription_service.update_remnawave_user = mock_update_remnawave
result = await modem_service.enable_modem(mock_db, sample_user, sample_subscription)
assert result.success
assert result.error is None
assert result.charged_amount == 10000
assert sample_subscription.modem_enabled is True
assert sample_subscription.device_limit == 3 # было 2, стало 3
async def test_enable_modem_insufficient_funds(self, monkeypatch):
"""Недостаточно средств для подключения."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_user.subscription = sample_subscription
sample_user.balance_kopeks = 1000 # недостаточно
mock_db = AsyncMock()
result = await modem_service.enable_modem(mock_db, sample_user, sample_subscription)
assert not result.success
assert result.error == ModemError.INSUFFICIENT_FUNDS
async def test_enable_modem_charge_error(self, monkeypatch):
"""Ошибка списания средств."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_user.subscription = sample_subscription
sample_user.balance_kopeks = 50000
mock_db = AsyncMock()
mock_subtract = AsyncMock(return_value=False) # ошибка списания
monkeypatch.setattr('app.services.modem_service.subtract_user_balance', mock_subtract)
result = await modem_service.enable_modem(mock_db, sample_user, sample_subscription)
assert not result.success
assert result.error == ModemError.CHARGE_ERROR
class TestModemServiceDisable:
"""Тесты отключения модема."""
async def test_disable_modem_success(self, monkeypatch):
"""Успешное отключение модема."""
modem_service, _ = create_modem_service(monkeypatch)
sample_user = create_sample_user()
sample_subscription = create_sample_subscription()
sample_subscription.modem_enabled = True
sample_subscription.device_limit = 3
sample_user.subscription = sample_subscription
mock_db = AsyncMock()
mock_update_remnawave = AsyncMock()
modem_service._subscription_service.update_remnawave_user = mock_update_remnawave
result = await modem_service.disable_modem(mock_db, sample_user, sample_subscription)
assert result.success
assert result.error is None
assert sample_subscription.modem_enabled is False
assert sample_subscription.device_limit == 2 # было 3, стало 2
class TestModemServiceSingleton:
"""Тесты singleton паттерна."""
def test_get_modem_service_returns_same_instance(self, monkeypatch):
"""get_modem_service возвращает один и тот же экземпляр."""
# Сбрасываем глобальный экземпляр
import app.services.modem_service as modem_module
modem_module._modem_service = None
mock_settings = create_mock_settings()
monkeypatch.setattr('app.services.modem_service.settings', mock_settings)
service1 = get_modem_service()
service2 = get_modem_service()
assert service1 is service2
class TestModemEnabledGetter:
"""Тесты безопасного получения статуса модема."""
def test_get_modem_enabled_true(self, monkeypatch):
"""Модем включён."""
modem_service, _ = create_modem_service(monkeypatch)
sample_subscription = create_sample_subscription()
sample_subscription.modem_enabled = True
assert modem_service.get_modem_enabled(sample_subscription) is True
def test_get_modem_enabled_false(self, monkeypatch):
"""Модем выключен."""
modem_service, _ = create_modem_service(monkeypatch)
sample_subscription = create_sample_subscription()
sample_subscription.modem_enabled = False
assert modem_service.get_modem_enabled(sample_subscription) is False
def test_get_modem_enabled_none_subscription(self, monkeypatch):
"""Подписка None."""
modem_service, _ = create_modem_service(monkeypatch)
assert modem_service.get_modem_enabled(None) is False
def test_get_modem_enabled_no_attribute(self, monkeypatch):
"""У подписки нет атрибута modem_enabled."""
modem_service, _ = create_modem_service(monkeypatch)
subscription = SimpleNamespace(id=1) # без modem_enabled
assert modem_service.get_modem_enabled(subscription) is False