from math import ceil from typing import Any from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder import sqlalchemy as sa from sqlalchemy.ext.asyncio import AsyncSession from config import USE_NEW_PAYMENT_FLOW from core.bootstrap import MODES_CONFIG 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, ) from database.models import User from handlers.buttons import BACK, CONFIRM_ADDON_BUTTON_TEXT, PAYMENT from middlewares.session import release_session_early 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_NO_EXTRA_PAYMENT_TEXT, ADDONS_PACK_SUCCESS_TEXT, INSUFFICIENT_FUNDS_RENEWAL_MSG, 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_addon_purchase_complete, process_addons_menu from logger import logger from ..buy.key_tariffs import calculate_config_price from ...keys.utils import build_key_callback, resolve_key from .utils import ( KeyAddonConfigState, build_addons_pack_screen_text, calc_remaining_ratio_seconds, format_devices_label, format_traffic_label, ) router = Router() def get_pack_flags() -> tuple[bool, bool, str]: mode = TARIFFS_CONFIG.get("KEY_ADDONS_PACK_MODE") or "" if not mode: return False, False, "" if mode == "traffic": return False, True, mode if mode == "devices": return True, False, mode if mode == "all": return True, True, mode logger.warning(f"Некорректный KEY_ADDONS_PACK_MODE: {mode!r}") return False, False, mode def get_override_value(overrides: Any, key: int) -> Any: if not isinstance(overrides, dict): return None if key in overrides: return overrides.get(key) return overrides.get(str(key)) def calc_pack_devices_price_rub(tariff: dict[str, Any], pack_devices: int | None) -> int: if pack_devices is None: return 0 pack_devices = int(pack_devices) overrides = tariff.get("device_overrides") or {} if pack_devices == 0: override = get_override_value(overrides, 0) return int(ceil(float(override))) if override is not None else 0 if pack_devices < 0: return 0 override = get_override_value(overrides, pack_devices) if override is not None: return int(ceil(float(override))) step_price = int(tariff.get("device_step_rub") or 0) return int(ceil(pack_devices * step_price)) def calc_pack_traffic_price_rub(tariff: dict[str, Any], pack_traffic_gb: int | None) -> int: if pack_traffic_gb is None: return 0 pack_traffic_gb = int(pack_traffic_gb) overrides = tariff.get("traffic_overrides") or {} if pack_traffic_gb == 0: override = get_override_value(overrides, 0) return int(ceil(float(override))) if override is not None else 0 if pack_traffic_gb < 0: return 0 override = get_override_value(overrides, pack_traffic_gb) if override is not None: return int(ceil(float(override))) step_price = int(tariff.get("traffic_step_rub") or 0) return int(ceil(pack_traffic_gb * step_price)) def calc_pack_full_price_rub( tariff: dict[str, Any], has_device_option: bool, has_traffic_option: bool, selected_devices: int | None, selected_traffic_gb: int | None, ) -> int: total = 0 if has_device_option: total += calc_pack_devices_price_rub(tariff, selected_devices if selected_devices is not None else None) if has_traffic_option: total += calc_pack_traffic_price_rub(tariff, selected_traffic_gb if selected_traffic_gb is not None else None) return int(total) 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 PACK_MODE 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") selected_devices = data.get("addon_selected_device_limit") selected_traffic_gb = data.get("addon_selected_traffic_gb") expiry_time = data.get("addon_expiry_time") if not email or not tariff_id: logger.warning(f"[ADDONS] PACK_MODE: нет 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] PACK_MODE: тариф {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 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 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 pack_devices, pack_traffic, pack_mode = get_pack_flags() has_device_option = pack_devices and bool(device_int_options) has_traffic_option = pack_traffic and bool(traffic_int_options) if has_device_option and current_devices is not None and int(current_devices) == 0: has_device_option = False selected_devices = None if has_traffic_option and current_traffic_gb is not None and int(current_traffic_gb) == 0: has_traffic_option = False selected_traffic_gb = None if not has_device_option: selected_devices = None if not has_traffic_option: selected_traffic_gb = None 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 else None current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None 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, ) try: base_price_for_current_int = int(base_price_for_current) if base_price_for_current is not None else 0 except (TypeError, ValueError): base_price_for_current_int = 0 recalc_enabled = bool( MODES_CONFIG.get( "KEY_ADDONS_RECALC_PRICE", TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False), ) ) diff_full = calc_pack_full_price_rub( tariff=tariff, has_device_option=has_device_option, has_traffic_option=has_traffic_option, selected_devices=int(selected_devices) if selected_devices is not None else None, selected_traffic_gb=int(selected_traffic_gb) if selected_traffic_gb is not None else None, ) if recalc_enabled: remaining_seconds, total_seconds = calc_remaining_ratio_seconds(expiry_time, tariff) extra_price = int((diff_full * remaining_seconds + total_seconds - 1) // total_seconds) else: extra_price = int(diff_full) logger.debug( "[ADDONS] PACK_MODE calculated prices: " f"base_price_for_current={base_price_for_current_int} diff_full={diff_full} " f"extra_price={extra_price} recalc_enabled={recalc_enabled} " f"has_device_option={has_device_option} has_traffic_option={has_traffic_option} pack_mode={pack_mode!r}" ) tg_id = callback.from_user.id language_code = getattr(callback.from_user, "language_code", None) 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) has_device_pack_selected = has_device_option and selected_devices is not None has_traffic_pack_selected = has_traffic_option and selected_traffic_gb is not None selected_devices_label = format_devices_label(selected_devices) if has_device_pack_selected else None selected_traffic_label = format_traffic_label(selected_traffic_gb) if has_traffic_pack_selected else None if has_device_pack_selected: current_devices_value = int(current_devices) if current_devices else 0 selected_devices_value = int(selected_devices) total_devices_value = ( 0 if current_devices_value <= 0 or selected_devices_value <= 0 else current_devices_value + selected_devices_value ) total_devices_label = format_devices_label(total_devices_value) else: total_devices_label = None if has_traffic_pack_selected: current_traffic_value = int(current_traffic_gb) if current_traffic_gb else 0 selected_traffic_value = int(selected_traffic_gb) total_after_gb = ( 0 if current_traffic_value <= 0 or selected_traffic_value <= 0 else current_traffic_value + selected_traffic_value ) total_traffic_label = format_traffic_label(total_after_gb) else: total_traffic_label = None text = build_addons_pack_screen_text( tariff_name=tariff_name, current_devices_label=current_devices_label, current_traffic_label=current_traffic_label if current_traffic_gb is not None else None, selected_devices_label=selected_devices_label, selected_traffic_label=selected_traffic_label, total_devices_label=total_devices_label, total_traffic_label=total_traffic_label, extra_price_text=extra_price_text, has_device_option=has_device_option, has_traffic_option=has_traffic_option, ) builder = InlineKeyboardBuilder() device_buttons = [] traffic_buttons = [] if has_device_option: for value in device_int_options: 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_option: for value in traffic_int_options: 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( "[ADDONS] PACK_MODE buttons: " f"devices={[b.text for b in device_buttons]} " f"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]) 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=build_key_callback("view_key", data.get("addon_key_client_id"), 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): key_ref = callback.data.split("|", 1)[1] key_obj = await resolve_key(session, callback.from_user.id, key_ref) email = key_obj.email if key_obj else key_ref logger.debug(f"[ADDONS] PACK_MODE 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] PACK_MODE: подписка {email} не найдена") await callback.message.answer("❌ Подписка не найдена.") return if record.get("tg_id") != callback.from_user.id: await callback.answer("Доступ запрещён.", show_alert=True) return tariff_id = record.get("tariff_id") if not tariff_id: logger.warning(f"[ADDONS] PACK_MODE: для подписки {email} не назначен тариф") await callback.message.answer("❌ Для этой подписки тариф не назначен, расширение недоступно.") return tariff = await get_tariff_by_id(session, int(tariff_id)) if not tariff: logger.error(f"[ADDONS] PACK_MODE: тариф {tariff_id} не найден для email={email}") await callback.message.answer("❌ Тариф не найден.") return if not tariff.get("configurable"): logger.info(f"[ADDONS] PACK_MODE: тариф {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 [] device_int_options: list[int] = [] for value in raw_device_options: try: device_int_options.append(int(value)) except (TypeError, ValueError): continue traffic_int_options: list[int] = [] for value in raw_traffic_options: try: traffic_int_options.append(int(value)) except (TypeError, ValueError): continue base_device_limit = cfg.get("base_device_limit") if base_device_limit is None: base_device_limit = tariff.get("device_limit") if base_device_limit is not None: try: base_device_int = int(base_device_limit) if base_device_int not in device_int_options: raw_device_options.append(base_device_int) device_int_options.append(base_device_int) except (TypeError, ValueError): pass device_overrides_cfg = cfg.get("device_price_overrides") or tariff.get("device_overrides") or {} if "0" in device_overrides_cfg and 0 not in device_int_options: raw_device_options.append(0) device_int_options.append(0) base_traffic_gb = cfg.get("base_traffic_gb") if base_traffic_gb is None: raw_limit = tariff.get("traffic_limit") if raw_limit: raw_limit = int(raw_limit) if raw_limit >= GB: base_traffic_gb = int(raw_limit / GB) else: base_traffic_gb = raw_limit if base_traffic_gb is not None: try: base_traffic_int = int(base_traffic_gb) if base_traffic_int not in traffic_int_options: raw_traffic_options.append(base_traffic_int) traffic_int_options.append(base_traffic_int) except (TypeError, ValueError): pass traffic_overrides_cfg = cfg.get("traffic_price_overrides") or tariff.get("traffic_overrides") or {} if "0" in traffic_overrides_cfg and 0 not in traffic_int_options: raw_traffic_options.append(0) traffic_int_options.append(0) 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] PACK_MODE start_key_addons options: " f"email={email} tariff_id={tariff_id} " f"device_options={device_options} traffic_options={traffic_options}" ) selected_device_limit_db = record.get("selected_device_limit") selected_traffic_limit_db = record.get("selected_traffic_limit") 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_value = 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_value) ) pack_devices, pack_traffic, pack_mode = get_pack_flags() has_device_pack = ( pack_devices and bool(device_options) and not (current_devices is not None and int(current_devices) == 0) ) has_traffic_pack = ( pack_traffic and bool(traffic_options) and not (current_traffic_gb is not None and int(current_traffic_gb) == 0) ) if not (has_device_pack or has_traffic_pack): logger.warning( f"[ADDONS] PACK_MODE: пакеты недоступны, уже максимальные параметры " f"email={email} current_devices={current_devices} current_traffic_gb={current_traffic_gb} " f"pack_mode={pack_mode!r}" ) await callback.message.answer("❌ Для этой подписки пакеты уже недоступны.") return cfg_for_state = dict(cfg) if device_options: cfg_for_state["device_options"] = device_options if traffic_options: cfg_for_state["traffic_options_gb"] = traffic_options await state.update_data( addon_key_email=email, addon_key_client_id=record.get("client_id"), 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_expiry_time=record.get("expiry_time"), addon_selected_device_limit=None, addon_selected_traffic_gb=None, ) 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() selected_devices = data.get("addon_selected_device_limit") logger.debug( "[ADDONS] PACK_MODE handle_addons_devices_choice: " f"tg_id={callback.from_user.id} email={data.get('addon_key_email')} " f"new_devices={new_devices} selected_devices={selected_devices}" ) if selected_devices is not None and int(selected_devices) == new_devices: await callback.answer() 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() selected_traffic_gb = data.get("addon_selected_traffic_gb") logger.debug( "[ADDONS] PACK_MODE handle_addons_traffic_choice: " f"tg_id={callback.from_user.id} email={data.get('addon_key_email')} " f"new_traffic_gb={new_traffic_gb} 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 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_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() if data.get("addon_confirm_in_progress"): await callback.answer("⏳ Уже обрабатываю...", show_alert=False) return await state.update_data(addon_confirm_in_progress=True) try: 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] PACK_MODE handle_addons_confirm: " 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 record = await get_key_details(session, email) if not record: logger.warning(f"[ADDONS] PACK_MODE: подписка {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] PACK_MODE: тариф {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 [] pack_devices, pack_traffic, pack_mode = get_pack_flags() has_device_option = pack_devices and bool(device_options) has_traffic_option = pack_traffic and bool(traffic_options) if has_device_option and current_devices is not None and int(current_devices) == 0: has_device_option = False selected_devices = None if has_traffic_option and current_traffic_gb is not None and int(current_traffic_gb) == 0: has_traffic_option = False selected_traffic_gb = None current_devices_for_price = int(current_devices) if current_devices is not None else None current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None 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, ) try: base_price_for_current_int = int(base_price_for_current) if base_price_for_current is not None else 0 except (TypeError, ValueError): base_price_for_current_int = 0 recalc_enabled = bool( MODES_CONFIG.get( "KEY_ADDONS_RECALC_PRICE", TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False), ) ) diff_full = calc_pack_full_price_rub( tariff=tariff, has_device_option=has_device_option, has_traffic_option=has_traffic_option, selected_devices=int(selected_devices) if selected_devices is not None else None, selected_traffic_gb=int(selected_traffic_gb) if selected_traffic_gb is not None else None, ) if recalc_enabled: remaining_seconds, total_seconds = calc_remaining_ratio_seconds(record.get("expiry_time"), tariff) extra_price = int((diff_full * remaining_seconds + total_seconds - 1) // total_seconds) total_price_after_purchase = base_price_for_current_int + extra_price else: extra_price = int(diff_full) total_price_after_purchase = base_price_for_current_int + diff_full logger.debug( "[ADDONS] PACK_MODE confirm prices: " f"base_price_for_current={base_price_for_current_int} diff_full={diff_full} " f"extra_price={extra_price} recalc_enabled={recalc_enabled} total_price_after_purchase={total_price_after_purchase} " f"has_device_option={has_device_option} has_traffic_option={has_traffic_option} " f"pack_mode={pack_mode!r}" ) if extra_price <= 0: logger.info(f"[ADDONS] PACK_MODE: extra_price <= 0, доплата не требуется, email={email}") await state.clear() await render_key_info(callback.message, session, email, "img/pic_view.jpg") await callback.answer(ADDONS_NO_EXTRA_PAYMENT_TEXT, show_alert=True) return balance = await get_balance(session, tg_id) logger.debug(f"[ADDONS] PACK_MODE 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] PACK_MODE: недостаточно средств " 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), "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] PACK_MODE 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"] device_limit_effective_current, traffic_limit_bytes_effective_current = await get_effective_limits_for_key( session=session, tariff_id=int(tariff_id), selected_device_limit=int(current_devices) if current_devices is not None else None, selected_traffic_gb=int(current_traffic_gb) if current_traffic_gb is not None else None, ) traffic_limit_gb_effective_current = ( int(traffic_limit_bytes_effective_current / GB) if traffic_limit_bytes_effective_current else 0 ) new_device_limit_effective = device_limit_effective_current new_traffic_limit_gb_effective = traffic_limit_gb_effective_current if has_device_option and selected_devices is not None: pack_devices_val = int(selected_devices) if pack_devices_val <= 0 or ( new_device_limit_effective is not None and new_device_limit_effective <= 0 ): new_device_limit_effective = 0 else: if new_device_limit_effective is None: new_device_limit_effective = pack_devices_val else: new_device_limit_effective = new_device_limit_effective + pack_devices_val if has_traffic_option and selected_traffic_gb is not None: pack_traffic_val = int(selected_traffic_gb) if pack_traffic_val <= 0 or new_traffic_limit_gb_effective <= 0: new_traffic_limit_gb_effective = 0 else: new_traffic_limit_gb_effective = new_traffic_limit_gb_effective + pack_traffic_val 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] PACK_MODE: не удалось определить текущую подгруппу: {error}") target_subgroup = tariff.get("subgroup_title") old_subgroup = current_subgroup total_gb = new_traffic_limit_gb_effective hwid_device_limit_to_set = new_device_limit_effective logger.debug( "[ADDONS] PACK_MODE 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 release_session_early(session) 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), ) result = await session.execute( sa.update(User) .where(User.tg_id == tg_id, User.balance >= extra_price) .values(balance=User.balance - extra_price) ) if (result.rowcount or 0) != 1: await session.rollback() await callback.answer("❌ Недостаточно средств.", show_alert=True) await state.clear() return await save_key_config_with_mode( session=session, email=email, selected_devices=new_device_limit_effective, selected_traffic_gb=new_traffic_limit_gb_effective, total_price=int(total_price_after_purchase), has_device_choice=has_device_option, has_traffic_choice=has_traffic_option, config_mode="pack", ) await session.commit() logger.info( "[ADDONS] PACK_MODE успешная покупка пакета: " f"tg_id={tg_id} email={email} extra_price={extra_price} " f"new_device_limit_effective={new_device_limit_effective} " f"new_traffic_limit_gb_effective={new_traffic_limit_gb_effective} " f"recalc_enabled={recalc_enabled} pack_mode={pack_mode!r}" ) await state.clear() intercepted = await process_addon_purchase_complete( chat_id=callback.from_user.id, session=session, email=email, message=callback.message, ) if not intercepted: await render_key_info(callback.message, session, email, "img/pic_view.jpg") await callback.answer(ADDONS_PACK_SUCCESS_TEXT, show_alert=True) except Exception as error: logger.error(f"[ADDONS] PACK_MODE ошибка при покупке пакета для {email}: {error}") await callback.message.answer("❌ Ошибка при обновлении подписки. Попробуйте позже.") await state.clear() finally: try: await state.update_data(addon_confirm_in_progress=False) except Exception: pass