Files
2026-01-22 00:04:43 +03:00

895 lines
36 KiB
Python

from math import ceil
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
from config import USE_NEW_PAYMENT_FLOW
from core.settings.tariffs_config import TARIFFS_CONFIG, normalize_tariff_config
from database import get_balance, get_key_details, get_tariff_by_id, save_key_config_with_mode, update_balance
from handlers.buttons import (
BACK,
CONFIRM_ADDON_BUTTON_TEXT,
DOWNGRADE_ADDON_BUTTON_TEXT,
DOWNGRADE_CONFIRM_BUTTON_TEXT,
PAYMENT,
)
from handlers.keys.key_view import render_key_info
from handlers.payments.currency_rates import format_for_user
from handlers.payments.fast_payment_flow import try_fast_payment_flow
from handlers.tariffs.tariff_display import GB, get_effective_limits_for_key
from handlers.texts import (
ADDONS_APPLIED_TEXT,
DOWNGRADE_INLINE_WARNING_TEXT,
DOWNGRADE_SAVED_TEXT,
DOWNGRADE_WARNING_TEXT,
INSUFFICIENT_FUNDS_RENEWAL_MSG,
NO_EXTRA_PAYMENT_TEXT,
UNLIMITED_DEVICES_LABEL,
UNLIMITED_TRAFFIC_LABEL,
)
from handlers.utils import edit_or_send_message
from hooks.hook_buttons import insert_hook_buttons
from hooks.processors import process_addons_menu
from logger import logger
from ..buy.key_tariffs import calculate_config_price
from .utils import (
KeyAddonConfigState,
build_addons_screen_text,
format_devices_label,
format_traffic_label,
is_not_downgrade,
)
router = Router()
async def render_addons_screen(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
data = await state.get_data()
email = data.get("addon_key_email")
tariff_id = data.get("addon_tariff_id")
cfg = data.get("addon_tariff_config") or {}
logger.debug(
f"[ADDONS] render_addons_screen start: tg_id={callback.from_user.id} "
f"email={email} tariff_id={tariff_id} data={data}"
)
current_devices = data.get("addon_current_device_limit")
current_traffic_gb = data.get("addon_current_traffic_gb")
original_price = int(data.get("addon_original_price") or 0)
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
if not email or not tariff_id:
logger.warning(f"[ADDONS] Нет email или tariff_id в состоянии: {data}")
await callback.message.answer("❌ Данные для изменения подписки не найдены.")
await state.clear()
return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] Тариф {tariff_id} не найден в render_addons_screen")
await callback.message.answer("❌ Тариф не найден.")
await state.clear()
return
tariff_name = tariff.get("name") or "подписка"
raw_device_options = cfg.get("device_options") or []
raw_traffic_options = cfg.get("traffic_options_gb") or []
try:
device_options = sorted(
raw_device_options,
key=lambda v: (int(v) == 0, int(v)),
)
except (TypeError, ValueError):
device_options = raw_device_options
try:
traffic_options = sorted(
raw_traffic_options,
key=lambda v: (int(v) == 0, int(v)),
)
except (TypeError, ValueError):
traffic_options = raw_traffic_options
device_int_options: list[int] = []
for value in device_options:
try:
device_int_options.append(int(value))
except (TypeError, ValueError):
continue
traffic_int_options: list[int] = []
for value in traffic_options:
try:
traffic_int_options.append(int(value))
except (TypeError, ValueError):
continue
has_device_option = bool(device_int_options)
has_traffic_option = bool(traffic_int_options)
has_device_choice = len(device_int_options) > 1
has_traffic_choice = len(traffic_int_options) > 1
if selected_devices is None and has_device_option:
if current_devices is not None and int(current_devices) in device_int_options:
selected_devices = int(current_devices)
elif device_int_options:
selected_devices = device_int_options[0]
if selected_traffic_gb is None and has_traffic_option:
if current_traffic_gb is not None and int(current_traffic_gb) in traffic_int_options:
selected_traffic_gb = int(current_traffic_gb)
elif traffic_int_options:
selected_traffic_gb = traffic_int_options[0]
logger.debug(
"[ADDONS] Limits before price: "
f"current_devices={current_devices} current_traffic_gb={current_traffic_gb} "
f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb} "
f"original_price={original_price}"
)
await state.update_data(
addon_selected_device_limit=selected_devices,
addon_selected_traffic_gb=selected_traffic_gb,
)
current_devices_for_price = int(current_devices) if current_devices is not None and has_device_option else None
current_traffic_for_price = (
int(current_traffic_gb) if current_traffic_gb is not None and has_traffic_option else None
)
base_price_for_current = calculate_config_price(
tariff=tariff,
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_for_price,
)
total_price = calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_devices) if selected_devices is not None and has_device_option else None,
selected_traffic_gb=int(selected_traffic_gb)
if selected_traffic_gb is not None and has_traffic_option
else None,
)
extra_price = max(0, total_price - base_price_for_current)
logger.debug(
"[ADDONS] Calculated prices: "
f"base_price_for_current={base_price_for_current} total_price={total_price} extra_price={extra_price} "
f"has_device_option={has_device_option} has_traffic_option={has_traffic_option}"
)
tg_id = callback.from_user.id
language_code = getattr(callback.from_user, "language_code", None)
total_price_text = await format_for_user(session, tg_id, float(total_price), language_code)
extra_price_text = await format_for_user(session, tg_id, float(extra_price), language_code)
current_devices_label = format_devices_label(current_devices)
current_traffic_label = format_traffic_label(current_traffic_gb)
new_devices_label = format_devices_label(selected_devices)
new_traffic_label = format_traffic_label(selected_traffic_gb)
downgrade_warning = None
devices_downgrade = False
traffic_downgrade = False
allow_downgrade = bool(TARIFFS_CONFIG.get("ALLOW_DOWNGRADE", True))
if allow_downgrade:
if has_device_choice and current_devices is not None and selected_devices is not None:
devices_downgrade = not is_not_downgrade(current_devices, selected_devices)
if has_traffic_choice and current_traffic_gb is not None and selected_traffic_gb is not None:
traffic_downgrade = not is_not_downgrade(current_traffic_gb, selected_traffic_gb)
if devices_downgrade or traffic_downgrade:
new_limits_parts = []
if has_device_choice:
new_limits_parts.append(new_devices_label)
if has_traffic_choice:
new_limits_parts.append(new_traffic_label)
new_limits_desc = ", ".join(new_limits_parts) if new_limits_parts else "выбранные параметры"
downgrade_warning = DOWNGRADE_INLINE_WARNING_TEXT.format(
total_price_text=total_price_text,
new_limits_desc=new_limits_desc,
)
logger.debug(
"[ADDONS] Downgrade flags: "
f"devices_downgrade={devices_downgrade} traffic_downgrade={traffic_downgrade} "
f"ALLOW_DOWNGRADE={allow_downgrade}"
)
text = build_addons_screen_text(
tariff_name=tariff_name,
current_devices_label=current_devices_label,
current_traffic_label=current_traffic_label,
new_devices_label=new_devices_label,
new_traffic_label=new_traffic_label,
has_device_choice=has_device_choice,
has_traffic_choice=has_traffic_choice,
total_price_text=total_price_text,
extra_price_text=extra_price_text,
downgrade_warning=downgrade_warning,
)
builder = InlineKeyboardBuilder()
device_buttons = []
traffic_buttons = []
if has_device_choice:
for value in device_int_options:
if not allow_downgrade and not is_not_downgrade(current_devices, value):
continue
is_selected = selected_devices is not None and int(value) == int(selected_devices)
mark = "" if is_selected else ""
if value == 0:
caption = f"{UNLIMITED_DEVICES_LABEL.capitalize()}{mark}"
else:
caption = f"{value} устройств{mark}"
device_buttons.append(
InlineKeyboardButton(
text=caption,
callback_data=f"key_addons_devices|{email}|{value}",
)
)
if has_traffic_choice:
for value in traffic_int_options:
if not allow_downgrade and not is_not_downgrade(current_traffic_gb, value):
continue
is_selected = selected_traffic_gb is not None and int(selected_traffic_gb) == int(value)
mark = "" if is_selected else ""
if value == 0:
caption = f"{UNLIMITED_TRAFFIC_LABEL.capitalize()}{mark}"
else:
caption = f"{value} ГБ{mark}"
traffic_buttons.append(
InlineKeyboardButton(
text=caption,
callback_data=f"key_addons_traffic|{email}|{value}",
)
)
logger.debug(
f"[ADDONS] Buttons: devices={[b.text for b in device_buttons]} traffic={[b.text for b in traffic_buttons]}"
)
if device_buttons and traffic_buttons:
max_len = max(len(device_buttons), len(traffic_buttons))
for i in range(max_len):
row = []
if i < len(device_buttons):
row.append(device_buttons[i])
if i < len(traffic_buttons):
row.append(traffic_buttons[i])
builder.row(*row)
elif device_buttons:
for i in range(0, len(device_buttons), 2):
builder.row(*device_buttons[i : i + 2])
elif traffic_buttons:
for i in range(0, len(traffic_buttons), 2):
builder.row(*traffic_buttons[i : i + 2])
if allow_downgrade and (devices_downgrade or traffic_downgrade):
builder.row(
InlineKeyboardButton(
text=DOWNGRADE_ADDON_BUTTON_TEXT,
callback_data="key_addons_downgrade",
)
)
else:
builder.row(
InlineKeyboardButton(
text=CONFIRM_ADDON_BUTTON_TEXT.format(amount=extra_price_text),
callback_data="key_addons_confirm",
)
)
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}"))
module_buttons = await process_addons_menu(email=email, session=session)
builder = insert_hook_buttons(builder, module_buttons)
await edit_or_send_message(
target_message=callback.message,
text=text,
reply_markup=builder.as_markup(),
)
await callback.answer()
@router.callback_query(F.data.startswith("key_addons|"))
async def start_key_addons(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
email = callback.data.split("|")[1]
logger.debug(f"[ADDONS] start_key_addons: tg_id={callback.from_user.id} email={email}")
record = await get_key_details(session, email)
if not record:
logger.warning(f"[ADDONS] Подписка {email} не найдена")
await callback.message.answer("❌ Подписка не найдена.")
return
tariff_id = record.get("tariff_id")
if not tariff_id:
logger.warning(f"[ADDONS] Для подписки {email} не назначен тариф")
await callback.message.answer("❌ Для этой подписки тариф не назначен, расширение недоступно.")
return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] Тариф {tariff_id} не найден для email={email}")
await callback.message.answer("❌ Тариф не найден.")
return
if not tariff.get("configurable"):
logger.info(f"[ADDONS] Тариф {tariff_id} не конфигурируемый, расширение недоступно")
await callback.message.answer("❌ Для этого тарифа расширение через конфигуратор недоступно.")
return
cfg = normalize_tariff_config(tariff)
raw_device_options = cfg.get("device_options") or tariff.get("device_options") or []
raw_traffic_options = cfg.get("traffic_options_gb") or tariff.get("traffic_options_gb") or []
try:
device_options = sorted(
raw_device_options,
key=lambda v: (int(v) == 0, int(v)),
)
except (TypeError, ValueError):
device_options = raw_device_options
try:
traffic_options = sorted(
raw_traffic_options,
key=lambda v: (int(v) == 0, int(v)),
)
except (TypeError, ValueError):
traffic_options = raw_traffic_options
logger.info(
"[ADDONS] start_key_addons options: "
f"email={email} tariff_id={tariff_id} "
f"device_options={device_options} traffic_options={traffic_options}"
)
if not device_options and not traffic_options:
logger.warning(f"[ADDONS] Пустой конфигуратор для тарифа {tariff_id}")
await callback.message.answer("❌ Конфигуратор для этого тарифа не настроен.")
return
selected_device_limit_db = record.get("selected_device_limit")
selected_traffic_limit_db = record.get("selected_traffic_limit")
original_price_db = record.get("selected_price_rub")
current_device_limit_db = record.get("current_device_limit")
current_traffic_limit_db = record.get("current_traffic_limit")
base_devices = tariff.get("device_limit")
base_devices = int(base_devices) if base_devices is not None else None
base_traffic_bytes = tariff.get("traffic_limit")
base_traffic_gb_from_tariff = int(base_traffic_bytes / GB) if base_traffic_bytes else None
current_devices = (
int(current_device_limit_db)
if current_device_limit_db is not None
else (int(selected_device_limit_db) if selected_device_limit_db is not None else base_devices)
)
current_traffic_gb = (
int(current_traffic_limit_db)
if current_traffic_limit_db is not None
else (int(selected_traffic_limit_db) if selected_traffic_limit_db is not None else base_traffic_gb_from_tariff)
)
has_device_option = bool(device_options)
has_traffic_option = bool(traffic_options)
current_devices_for_price = int(current_devices) if current_devices is not None and has_device_option else None
current_traffic_gb_for_price = (
int(current_traffic_gb) if current_traffic_gb is not None and has_traffic_option else None
)
config_price_for_current = calculate_config_price(
tariff=tariff,
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_gb_for_price,
)
try:
original_price_from_db = int(original_price_db) if original_price_db is not None else 0
except (TypeError, ValueError):
original_price_from_db = 0
original_price = original_price_from_db or int(config_price_for_current)
logger.debug(
"[ADDONS] start_key_addons state: "
f"email={email} tariff_id={tariff_id} current_devices={current_devices} current_traffic_gb={current_traffic_gb} "
f"config_price_for_current={config_price_for_current} original_price_db={original_price_db} "
f"original_price={original_price}"
)
cfg_for_state = dict(cfg)
cfg_for_state["device_options"] = device_options
cfg_for_state["traffic_options_gb"] = traffic_options
await state.update_data(
addon_key_email=email,
addon_tariff_id=int(tariff_id),
addon_tariff_config=cfg_for_state,
addon_current_device_limit=current_devices,
addon_current_traffic_gb=current_traffic_gb,
addon_original_price=original_price,
addon_selected_device_limit=current_devices,
addon_selected_traffic_gb=current_traffic_gb,
)
await state.set_state(KeyAddonConfigState.configuring)
await render_addons_screen(callback, state, session)
@router.callback_query(F.data.startswith("key_addons_devices|"), KeyAddonConfigState.configuring)
async def handle_addons_devices_choice(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
parts = callback.data.split("|", 2)
if len(parts) < 3:
await callback.answer("Некорректные данные", show_alert=True)
return
new_devices = int(parts[2])
data = await state.get_data()
current_devices = data.get("addon_current_device_limit")
selected_devices = data.get("addon_selected_device_limit")
logger.debug(
"[ADDONS] handle_addons_devices_choice: "
f"tg_id={callback.from_user.id} email={data.get('addon_key_email')} "
f"new_devices={new_devices} current_devices={current_devices} selected_devices={selected_devices}"
)
if selected_devices is not None and int(selected_devices) == new_devices:
await callback.answer()
return
allow_downgrade = bool(TARIFFS_CONFIG.get("ALLOW_DOWNGRADE", True))
if not allow_downgrade and not is_not_downgrade(current_devices, new_devices):
await callback.answer("Нельзя снижать лимит устройств, только увеличивать.", show_alert=True)
return
await state.update_data(addon_selected_device_limit=new_devices)
await render_addons_screen(callback, state, session)
@router.callback_query(F.data.startswith("key_addons_traffic|"), KeyAddonConfigState.configuring)
async def handle_addons_traffic_choice(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
parts = callback.data.split("|", 2)
if len(parts) < 3:
await callback.answer("Некорректные данные", show_alert=True)
return
new_traffic_gb = int(parts[2])
data = await state.get_data()
current_traffic_gb = data.get("addon_current_traffic_gb")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
logger.debug(
"[ADDONS] handle_addons_traffic_choice: "
f"tg_id={callback.from_user.id} email={data.get('addon_key_email')} "
f"new_traffic_gb={new_traffic_gb} current_traffic_gb={current_traffic_gb} "
f"selected_traffic_gb={selected_traffic_gb}"
)
if selected_traffic_gb is not None and int(selected_traffic_gb) == new_traffic_gb:
await callback.answer()
return
allow_downgrade = bool(TARIFFS_CONFIG.get("ALLOW_DOWNGRADE", True))
if not allow_downgrade and not is_not_downgrade(current_traffic_gb, new_traffic_gb):
await callback.answer("Нельзя снижать лимит трафика, только увеличивать.", show_alert=True)
return
await state.update_data(addon_selected_traffic_gb=new_traffic_gb)
await render_addons_screen(callback, state, session)
@router.callback_query(F.data == "key_addons_downgrade", KeyAddonConfigState.configuring)
async def handle_addons_downgrade(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
tg_id = callback.from_user.id
data = await state.get_data()
email = data.get("addon_key_email")
tariff_id = data.get("addon_tariff_id")
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
current_devices = data.get("addon_current_device_limit")
current_traffic_gb = data.get("addon_current_traffic_gb")
logger.info(
"[ADDONS] handle_addons_downgrade: "
f"tg_id={tg_id} email={email} tariff_id={tariff_id} "
f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb} "
f"current_devices={current_devices} current_traffic_gb={current_traffic_gb}"
)
if not email or not tariff_id:
await callback.message.answer("❌ Данные для изменения подписки не найдены.")
await state.clear()
return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] Тариф {tariff_id} не найден в handle_addons_downgrade")
await callback.message.answer("❌ Тариф не найден.")
await state.clear()
return
cfg = data.get("addon_tariff_config") or {}
device_options = cfg.get("device_options") or []
traffic_options = cfg.get("traffic_options_gb") or []
has_device_option = bool(device_options)
has_traffic_option = bool(traffic_options)
has_device_choice = len(device_options) > 1
has_traffic_choice = len(traffic_options) > 1
total_price = calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_devices) if selected_devices is not None and has_device_option else None,
selected_traffic_gb=int(selected_traffic_gb)
if selected_traffic_gb is not None and has_traffic_option
else None,
)
logger.debug(
"[ADDONS] Downgrade price: "
f"total_price={total_price} has_device_option={has_device_option} has_traffic_option={has_traffic_option}"
)
language_code = getattr(callback.from_user, "language_code", None)
total_price_text = await format_for_user(session, tg_id, float(total_price), language_code)
current_devices_label = format_devices_label(current_devices)
current_traffic_label = format_traffic_label(current_traffic_gb)
new_devices_label = format_devices_label(selected_devices)
new_traffic_label = format_traffic_label(selected_traffic_gb)
text = DOWNGRADE_WARNING_TEXT.format(
tariff_name=tariff.get("name") or "подписка",
current_devices_label=current_devices_label if has_device_choice else "по умолчанию",
current_traffic_label=current_traffic_label if has_traffic_choice else "по умолчанию",
new_devices_label=new_devices_label if has_device_choice else "по умолчанию",
new_traffic_label=new_traffic_label if has_traffic_choice else "по умолчанию",
total_price_text=total_price_text,
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text=DOWNGRADE_CONFIRM_BUTTON_TEXT,
callback_data="key_addons_downgrade_apply",
)
)
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"key_addons|{email}"))
await edit_or_send_message(
target_message=callback.message,
text=text,
reply_markup=builder.as_markup(),
)
await callback.answer()
@router.callback_query(F.data == "key_addons_downgrade_apply", KeyAddonConfigState.configuring)
async def handle_addons_downgrade_apply(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
data = await state.get_data()
email = data.get("addon_key_email")
tariff_id = data.get("addon_tariff_id")
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
logger.info(
"[ADDONS] handle_addons_downgrade_apply: "
f"tg_id={callback.from_user.id} email={email} tariff_id={tariff_id} "
f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb}"
)
if not email or not tariff_id:
await callback.message.answer("❌ Данные для изменения подписки не найдены.")
await state.clear()
return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] Тариф {tariff_id} не найден в handle_addons_downgrade_apply")
await callback.message.answer("❌ Тариф не найден.")
await state.clear()
return
cfg = data.get("addon_tariff_config") or {}
device_options = cfg.get("device_options") or []
traffic_options = cfg.get("traffic_options_gb") or []
has_device_option = bool(device_options)
has_traffic_option = bool(traffic_options)
has_device_choice = len(device_options) > 1
has_traffic_choice = len(traffic_options) > 1
total_price = calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_devices) if selected_devices is not None and has_device_option else None,
selected_traffic_gb=int(selected_traffic_gb)
if selected_traffic_gb is not None and has_traffic_option
else None,
)
logger.debug(
"[ADDONS] Downgrade apply price: "
f"total_price={total_price} has_device_choice={has_device_choice} has_traffic_choice={has_traffic_choice}"
)
try:
await save_key_config_with_mode(
session=session,
email=email,
selected_devices=selected_devices,
selected_traffic_gb=selected_traffic_gb,
total_price=int(total_price),
has_device_choice=has_device_choice,
has_traffic_choice=has_traffic_choice,
config_mode="downgrade",
)
await session.commit()
except Exception as error:
logger.error(f"[ADDONS] Ошибка при сохранении будущих условий для {email}: {error}")
await callback.message.answer("❌ Ошибка при сохранении новых условий. Попробуйте позже.")
await state.clear()
return
await state.clear()
await render_key_info(callback.message, session, email, "img/pic_view.jpg")
await callback.answer(DOWNGRADE_SAVED_TEXT, show_alert=True)
@router.callback_query(F.data == "key_addons_confirm", KeyAddonConfigState.configuring)
async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
from handlers.keys.operations import renew_key_in_cluster
tg_id = callback.from_user.id
data = await state.get_data()
email = data.get("addon_key_email")
tariff_id = data.get("addon_tariff_id")
original_price = int(data.get("addon_original_price") or 0)
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
current_devices = data.get("addon_current_device_limit")
current_traffic_gb = data.get("addon_current_traffic_gb")
logger.info(
"[ADDONS] handle_addons_confirm: "
f"tg_id={tg_id} email={email} tariff_id={tariff_id} original_price={original_price} "
f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb} "
f"current_devices={current_devices} current_traffic_gb={current_traffic_gb}"
)
if not email or not tariff_id:
await callback.message.answer("❌ Данные для изменения подписки не найдены.")
await state.clear()
return
record = await get_key_details(session, email)
if not record:
logger.warning(f"[ADDONS] Подписка {email} не найдена в handle_addons_confirm")
await callback.message.answer("❌ Подписка не найдена.")
await state.clear()
return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] Тариф {tariff_id} не найден в handle_addons_confirm")
await callback.message.answer("❌ Тариф не найден.")
await state.clear()
return
cfg = data.get("addon_tariff_config") or {}
device_options = cfg.get("device_options") or []
traffic_options = cfg.get("traffic_options_gb") or []
has_device_option = bool(device_options)
has_traffic_option = bool(traffic_options)
has_device_choice = len(device_options) > 1
has_traffic_choice = len(traffic_options) > 1
current_devices_for_price = int(current_devices) if current_devices is not None and has_device_option else None
current_traffic_for_price = (
int(current_traffic_gb) if current_traffic_gb is not None and has_traffic_option else None
)
base_price_for_current = calculate_config_price(
tariff=tariff,
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_for_price,
)
total_price = calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_devices) if selected_devices is not None and has_device_option else None,
selected_traffic_gb=int(selected_traffic_gb)
if selected_traffic_gb is not None and has_traffic_option
else None,
)
extra_price = max(0, total_price - base_price_for_current)
logger.debug(
"[ADDONS] Confirm prices: "
f"base_price_for_current={base_price_for_current} total_price={total_price} "
f"original_price={original_price} extra_price={extra_price} "
f"has_device_option={has_device_option} has_traffic_option={has_traffic_option}"
)
allow_downgrade = bool(TARIFFS_CONFIG.get("ALLOW_DOWNGRADE", True))
devices_downgrade = (
allow_downgrade
and has_device_choice
and current_devices is not None
and selected_devices is not None
and not is_not_downgrade(current_devices, selected_devices)
)
traffic_downgrade = (
allow_downgrade
and has_traffic_choice
and current_traffic_gb is not None
and selected_traffic_gb is not None
and not is_not_downgrade(current_traffic_gb, selected_traffic_gb)
)
logger.debug(
f"[ADDONS] Confirm downgrade flags: devices_downgrade={devices_downgrade} traffic_downgrade={traffic_downgrade}"
)
if devices_downgrade or traffic_downgrade:
await handle_addons_downgrade(callback, state, session)
return
if extra_price <= 0:
logger.info(f"[ADDONS] extra_price <= 0, доплата не требуется, email={email}")
await state.clear()
await render_key_info(callback.message, session, email, "img/pic_view.jpg")
await callback.answer(NO_EXTRA_PAYMENT_TEXT, show_alert=True)
return
balance = await get_balance(session, tg_id)
logger.debug(f"[ADDONS] Balance check: tg_id={tg_id} balance={balance} extra_price={extra_price}")
if balance < extra_price:
required_amount = ceil(extra_price - balance)
language_code = getattr(callback.from_user, "language_code", None)
required_amount_text = await format_for_user(session, tg_id, float(required_amount), language_code)
logger.info(
"[ADDONS] Недостаточно средств: "
f"balance={balance} extra_price={extra_price} required_amount={required_amount} "
f"tg_id={tg_id} USE_NEW_PAYMENT_FLOW={USE_NEW_PAYMENT_FLOW}"
)
if USE_NEW_PAYMENT_FLOW:
handled = await try_fast_payment_flow(
callback,
session,
state,
tg_id=tg_id,
temp_key="waiting_for_addons_payment",
temp_payload={
"email": email,
"tariff_id": int(tariff_id),
"original_price": original_price,
"selected_device_limit": selected_devices,
"selected_traffic_gb": selected_traffic_gb,
"current_device_limit": current_devices,
"current_traffic_gb": current_traffic_gb,
"required_amount": required_amount,
},
required_amount=required_amount,
)
logger.debug(f"[ADDONS] try_fast_payment_flow handled={handled} tg_id={tg_id} email={email}")
if handled:
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
await edit_or_send_message(
target_message=callback.message,
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount_text),
reply_markup=builder.as_markup(),
)
return
try:
expiry_time = record["expiry_time"]
client_id = record["client_id"]
server_id = record["server_id"]
selected_traffic_gb_for_effective = (
int(selected_traffic_gb) if selected_traffic_gb is not None and has_traffic_option else 0
)
current_subgroup = None
try:
current_tariff_id = record.get("tariff_id")
if current_tariff_id:
current_tariff = await get_tariff_by_id(session, int(current_tariff_id))
if current_tariff:
current_subgroup = current_tariff.get("subgroup_title")
except Exception as error:
logger.warning(f"[ADDONS] Не удалось определить текущую подгруппу: {error}")
target_subgroup = tariff.get("subgroup_title")
old_subgroup = current_subgroup
device_limit_effective_new, traffic_limit_bytes_effective_new = await get_effective_limits_for_key(
session=session,
tariff_id=int(tariff_id),
selected_device_limit=int(selected_devices) if selected_devices is not None and has_device_option else None,
selected_traffic_gb=selected_traffic_gb_for_effective,
)
traffic_limit_gb_effective = (
int(traffic_limit_bytes_effective_new / GB) if traffic_limit_bytes_effective_new else 0
)
total_gb = traffic_limit_gb_effective
hwid_device_limit_to_set = device_limit_effective_new
logger.debug(
"[ADDONS] renew_key_in_cluster params: "
f"server_id={server_id} email={email} client_id={client_id} total_gb={total_gb} "
f"hwid_device_limit_to_set={hwid_device_limit_to_set} target_subgroup={target_subgroup} "
f"old_subgroup={old_subgroup}"
)
await renew_key_in_cluster(
cluster_id=server_id,
email=email,
client_id=client_id,
new_expiry_time=expiry_time,
total_gb=total_gb,
session=session,
hwid_device_limit=hwid_device_limit_to_set,
reset_traffic=False,
target_subgroup=target_subgroup,
old_subgroup=old_subgroup,
plan=int(tariff_id),
)
await save_key_config_with_mode(
session=session,
email=email,
selected_devices=selected_devices,
selected_traffic_gb=selected_traffic_gb,
total_price=int(total_price),
has_device_choice=has_device_choice,
has_traffic_choice=has_traffic_choice,
config_mode="addon",
)
await update_balance(session, tg_id, -extra_price)
await session.commit()
logger.info(
"[ADDONS] Успешное применение расширения: "
f"tg_id={tg_id} email={email} total_price={total_price} extra_price={extra_price}"
)
await state.clear()
await render_key_info(callback.message, session, email, "img/pic_view.jpg")
await callback.answer(ADDONS_APPLIED_TEXT, show_alert=True)
except Exception as error:
logger.error(f"[ADDONS] Ошибка при применении расширения подписки для {email}: {error}")
await callback.message.answer("❌ Ошибка при обновлении подписки. Попробуйте позже.")
await state.clear()