mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-13 07:30:28 +00:00
Рефакторинг архитектуры управления модемом: - Создан сервис app/services/modem_service.py: - ModemService с бизнес-логикой подключения/отключения - ModemError enum для типизации ошибок - ModemPriceInfo, ModemOperationResult dataclass'ы - Константы MODEM_WARNING_DAYS_* для уровней предупреждений
389 lines
13 KiB
Python
389 lines
13 KiB
Python
"""
|
||
Хендлеры для управления модемом в подписке.
|
||
|
||
Модем - это дополнительное устройство, которое можно подключить к подписке
|
||
за отдельную плату. При подключении увеличивается лимит устройств.
|
||
"""
|
||
|
||
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"
|
||
)
|