Files
remnawave-bedolaga-telegram…/app/handlers/subscription/modem.py
gy9vin da46e39c61 refactor(modem): рефакторинг модуля управления модемом
Рефакторинг архитектуры управления модемом:

- Создан сервис app/services/modem_service.py:
  - ModemService с бизнес-логикой подключения/отключения
  - ModemError enum для типизации ошибок
  - ModemPriceInfo, ModemOperationResult dataclass'ы
  - Константы MODEM_WARNING_DAYS_* для уровней предупреждений
2025-12-25 18:44:27 +03:00

389 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Хендлеры для управления модемом в подписке.
Модем - это дополнительное устройство, которое можно подключить к подписке
за отдельную плату. При подключении увеличивается лимит устройств.
"""
import logging
from aiogram import Dispatcher, types, F
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.keyboards.inline import get_back_keyboard, get_insufficient_balance_keyboard
from app.localization.texts import get_texts
from app.services.modem_service import (
get_modem_service,
ModemError,
MODEM_WARNING_DAYS_CRITICAL,
MODEM_WARNING_DAYS_INFO,
)
from app.utils.decorators import error_handler, modem_available
logger = logging.getLogger(__name__)
def get_modem_keyboard(language: str, modem_enabled: bool):
"""Клавиатура управления модемом."""
texts = get_texts(language)
keyboard = []
if modem_enabled:
keyboard.append([
types.InlineKeyboardButton(
text=texts.t("MODEM_DISABLE_BUTTON", "Отключить модем"),
callback_data="modem_disable"
)
])
else:
keyboard.append([
types.InlineKeyboardButton(
text=texts.t("MODEM_ENABLE_BUTTON", "Подключить модем"),
callback_data="modem_enable"
)
])
keyboard.append([
types.InlineKeyboardButton(
text=texts.BACK,
callback_data="subscription_settings"
)
])
return types.InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_modem_confirm_keyboard(language: str):
"""Клавиатура подтверждения подключения модема."""
texts = get_texts(language)
return types.InlineKeyboardMarkup(inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t("MODEM_CONFIRM_BUTTON", "Подтвердить подключение"),
callback_data="modem_confirm"
)
],
[
types.InlineKeyboardButton(
text=texts.CANCEL,
callback_data="subscription_modem"
)
]
])
@error_handler
@modem_available()
async def handle_modem_menu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Показывает меню управления модемом."""
texts = get_texts(db_user.language)
subscription = db_user.subscription
service = get_modem_service()
modem_enabled = service.get_modem_enabled(subscription)
modem_price = settings.get_modem_price_per_month()
if modem_enabled:
status_text = texts.t("MODEM_STATUS_ENABLED", "Подключен")
info_text = texts.t(
"MODEM_INFO_ENABLED",
(
"<b>Модем</b>\n\n"
"Статус: {status}\n\n"
"Модем подключен к вашей подписке.\n"
"Ежемесячная плата: {price}\n\n"
"При отключении модема возврат средств не производится."
),
).format(
status=status_text,
price=texts.format_price(modem_price),
)
else:
status_text = texts.t("MODEM_STATUS_DISABLED", "Не подключен")
info_text = texts.t(
"MODEM_INFO_DISABLED",
(
"<b>Модем</b>\n\n"
"Статус: {status}\n\n"
"Подключите модем к вашей подписке.\n"
"Ежемесячная плата: {price}\n\n"
"При подключении модема будет добавлено дополнительное устройство."
),
).format(
status=status_text,
price=texts.format_price(modem_price),
)
await callback.message.edit_text(
info_text,
reply_markup=get_modem_keyboard(db_user.language, modem_enabled),
parse_mode="HTML"
)
await callback.answer()
@error_handler
@modem_available(for_enable=True)
async def handle_modem_enable(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Обработчик подключения модема - показывает информацию о цене."""
texts = get_texts(db_user.language)
subscription = db_user.subscription
service = get_modem_service()
price_info = service.calculate_price(subscription)
modem_price_per_month = settings.get_modem_price_per_month()
has_funds, missing_kopeks = service.check_balance(db_user, price_info.final_price)
if not has_funds:
if price_info.has_discount:
required_text = (
f"{texts.format_price(price_info.final_price)} "
f"(за {price_info.charged_months} мес, скидка {price_info.discount_percent}%)"
)
else:
required_text = (
f"{texts.format_price(price_info.final_price)} "
f"(за {price_info.charged_months} мес)"
)
message_text = texts.t(
"MODEM_INSUFFICIENT_FUNDS",
(
"<b>Недостаточно средств</b>\n\n"
"Стоимость подключения модема: {required}\n"
"На балансе: {balance}\n"
"Не хватает: {missing}\n\n"
"Выберите способ пополнения."
),
).format(
required=required_text,
balance=texts.format_price(db_user.balance_kopeks),
missing=texts.format_price(missing_kopeks),
)
await callback.message.edit_text(
message_text,
reply_markup=get_insufficient_balance_keyboard(
db_user.language,
amount_kopeks=missing_kopeks,
),
parse_mode="HTML",
)
await callback.answer()
return
warning_level = service.get_period_warning_level(price_info.remaining_days)
if warning_level == "critical":
warning_text = texts.t(
"MODEM_SHORT_PERIOD_WARNING",
"\n<b>Внимание!</b> До окончания подписки осталось всего <b>{days} дн.</b>\n"
"После продления подписки модем нужно будет оплатить заново!"
).format(days=price_info.remaining_days)
elif warning_level == "info":
warning_text = texts.t(
"MODEM_PERIOD_NOTE",
"\nДо окончания подписки: <b>{days} дн.</b>\n"
"После продления модем нужно будет оплатить заново."
).format(days=price_info.remaining_days)
else:
warning_text = ""
if price_info.has_discount:
price_text = texts.t(
"MODEM_PRICE_WITH_DISCOUNT",
"Стоимость: <s>{base_price}</s> <b>{final_price}</b> (за {months} мес)\n"
"Скидка {discount}%: -{discount_amount}"
).format(
base_price=texts.format_price(price_info.base_price),
final_price=texts.format_price(price_info.final_price),
months=price_info.charged_months,
discount=price_info.discount_percent,
discount_amount=texts.format_price(price_info.discount_amount),
)
else:
price_text = texts.t(
"MODEM_PRICE_NO_DISCOUNT",
"Стоимость: {price} (за {months} мес)"
).format(
price=texts.format_price(price_info.final_price),
months=price_info.charged_months,
)
confirm_text = texts.t(
"MODEM_CONFIRM_ENABLE_BASE",
(
"<b>Подтверждение подключения модема</b>\n\n"
"{price_text}\n\n"
"При подключении модема:\n"
"К подписке добавится дополнительное устройство\n"
"Ежемесячная плата увеличится на {monthly_price}\n\n"
"Подтвердить подключение?"
),
).format(
price_text=price_text,
monthly_price=texts.format_price(modem_price_per_month),
)
end_date_str = price_info.end_date.strftime("%d.%m.%Y")
period_info = texts.t(
"MODEM_PERIOD_INFO",
"\nМодем действует до: <b>{end_date}</b> ({days} дн.)"
).format(end_date=end_date_str, days=price_info.remaining_days)
confirm_text += period_info + warning_text
await callback.message.edit_text(
confirm_text,
reply_markup=get_modem_confirm_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
@error_handler
@modem_available(for_enable=True)
async def handle_modem_confirm(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Подтверждение и активация модема."""
texts = get_texts(db_user.language)
subscription = db_user.subscription
service = get_modem_service()
result = await service.enable_modem(db, db_user, subscription)
if not result.success:
error_messages = {
ModemError.INSUFFICIENT_FUNDS: texts.t(
"MODEM_INSUFFICIENT_FUNDS_SHORT",
"Недостаточно средств на балансе"
),
ModemError.CHARGE_ERROR: texts.t(
"PAYMENT_CHARGE_ERROR",
"Ошибка списания средств"
),
ModemError.UPDATE_ERROR: texts.ERROR,
}
error_text = error_messages.get(result.error, texts.ERROR)
if result.error == ModemError.INSUFFICIENT_FUNDS:
await callback.message.edit_text(
error_text,
reply_markup=get_back_keyboard(db_user.language, "modem_enable"),
parse_mode="HTML"
)
else:
await callback.answer(error_text, show_alert=True)
return
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_subscription_update_notification(
db, db_user, subscription, "modem", False, True, result.charged_amount
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о подключении модема: {e}")
success_text = texts.t(
"MODEM_ENABLED_SUCCESS",
(
"<b>Модем успешно подключен!</b>\n\n"
"Модем активирован\n"
"Добавлено устройство для модема\n"
),
)
if result.charged_amount > 0:
success_text += texts.t(
"MODEM_CHARGED",
"Списано: {amount}",
).format(amount=texts.format_price(result.charged_amount))
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language, "subscription_settings"),
parse_mode="HTML"
)
await callback.answer()
@error_handler
@modem_available(for_disable=True)
async def handle_modem_disable(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Отключение модема."""
texts = get_texts(db_user.language)
subscription = db_user.subscription
service = get_modem_service()
result = await service.disable_modem(db, db_user, subscription)
if not result.success:
await callback.answer(texts.ERROR, show_alert=True)
return
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_subscription_update_notification(
db, db_user, subscription, "modem", True, False, 0
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об отключении модема: {e}")
success_text = texts.t(
"MODEM_DISABLED_SUCCESS",
(
"<b>Модем отключен</b>\n\n"
"Модем деактивирован\n"
"Возврат средств не производится"
),
)
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language, "subscription_settings"),
parse_mode="HTML"
)
await callback.answer()
def register_modem_handlers(dp: Dispatcher):
"""Регистрация обработчиков модема."""
dp.callback_query.register(
handle_modem_menu,
F.data == "subscription_modem"
)
dp.callback_query.register(
handle_modem_enable,
F.data == "modem_enable"
)
dp.callback_query.register(
handle_modem_confirm,
F.data == "modem_confirm"
)
dp.callback_query.register(
handle_modem_disable,
F.data == "modem_disable"
)