From 4cc6778e453adbd4e094ffa1e953aedeb5eb7754 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 31 Oct 2025 17:42:04 +0300 Subject: [PATCH] Revert "Preserve saved cart metadata when normalizing devices" --- .env.example | 2 - README.md | 1 - app/config.py | 8 +- app/handlers/admin/users.py | 23 +- app/handlers/simple_subscription.py | 125 ++---- app/handlers/subscription/autopay.py | 71 ++- app/handlers/subscription/countries.py | 6 - app/handlers/subscription/devices.py | 28 -- app/handlers/subscription/pricing.py | 61 +-- app/handlers/subscription/purchase.py | 454 +++++++------------- app/handlers/subscription/summary.py | 59 --- app/handlers/subscription/traffic.py | 19 +- app/keyboards/inline.py | 35 +- app/localization/locales/en.json | 3 - app/localization/locales/ru.json | 3 - app/services/monitoring_service.py | 13 +- app/services/remnawave_service.py | 28 +- app/services/subscription_service.py | 34 +- app/services/system_settings_service.py | 7 - app/utils/subscription_utils.py | 16 - tests/test_subscription_cart_integration.py | 74 ---- 21 files changed, 275 insertions(+), 795 deletions(-) delete mode 100644 app/handlers/subscription/summary.py diff --git a/.env.example b/.env.example index e8c6ee68..a18ea009 100644 --- a/.env.example +++ b/.env.example @@ -146,8 +146,6 @@ TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true, # Цена за дополнительное устройство (DEFAULT_DEVICE_LIMIT идет бесплатно!) PRICE_PER_DEVICE=10000 -# Включить выбор количества устройств при покупке и продлении -DEVICES_SELECTION_ENABLED=true # ===== РЕФЕРАЛЬНАЯ СИСТЕМА ===== REFERRAL_PROGRAM_ENABLED=true diff --git a/README.md b/README.md index 60e1e893..aca837a4 100644 --- a/README.md +++ b/README.md @@ -569,7 +569,6 @@ BASE_PROMO_GROUP_PERIOD_DISCOUNTS=60:10,90:20,180:40,360:70 TRAFFIC_PACKAGES_CONFIG="5:2000:false,10:3500:false,25:7000:false,50:11000:true,100:15000:true,0:20000:true" PRICE_PER_DEVICE=5000 -DEVICES_SELECTION_ENABLED=true # ===== РЕФЕРАЛЬНАЯ СИСТЕМА ===== REFERRAL_PROGRAM_ENABLED=true diff --git a/app/config.py b/app/config.py index f5188453..788bc8e6 100644 --- a/app/config.py +++ b/app/config.py @@ -123,11 +123,10 @@ class Settings(BaseSettings): PRICE_TRAFFIC_500GB: int = 19000 PRICE_TRAFFIC_1000GB: int = 19500 PRICE_TRAFFIC_UNLIMITED: int = 20000 - + TRAFFIC_PACKAGES_CONFIG: str = "" PRICE_PER_DEVICE: int = 5000 - DEVICES_SELECTION_ENABLED: bool = True BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED: bool = False BASE_PROMO_GROUP_PERIOD_DISCOUNTS: str = "" @@ -798,12 +797,9 @@ class Settings(BaseSettings): def is_traffic_fixed(self) -> bool: return self.TRAFFIC_SELECTION_MODE.lower() == "fixed" - + def get_fixed_traffic_limit(self) -> int: return self.FIXED_TRAFFIC_LIMIT_GB - - def is_devices_selection_enabled(self) -> bool: - return self.DEVICES_SELECTION_ENABLED def is_yookassa_enabled(self) -> bool: return (self.YOOKASSA_ENABLED and diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index 5354a01c..a1f44f7c 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -35,7 +35,6 @@ from app.database.crud.server_squad import ( get_server_ids_by_uuids, ) from app.services.subscription_service import SubscriptionService -from app.utils.subscription_utils import resolve_hwid_device_limit logger = logging.getLogger(__name__) @@ -3833,50 +3832,40 @@ async def admin_buy_subscription_execute( from app.external.remnawave_api import UserStatus, TrafficLimitStrategy remnawave_service = RemnaWaveService() - hwid_limit = resolve_hwid_device_limit(subscription) - if target_user.remnawave_uuid: async with remnawave_service.get_api_client() as api: - update_kwargs = dict( + remnawave_user = await api.update_user( uuid=target_user.remnawave_uuid, status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED, expire_at=subscription.end_date, traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0, traffic_limit_strategy=TrafficLimitStrategy.MONTH, + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=target_user.full_name, username=target_user.username, telegram_id=target_user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - update_kwargs['hwid_device_limit'] = hwid_limit - - remnawave_user = await api.update_user(**update_kwargs) else: username = f"user_{target_user.telegram_id}" async with remnawave_service.get_api_client() as api: - create_kwargs = dict( + remnawave_user = await api.create_user( username=username, expire_at=subscription.end_date, status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED, traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0, traffic_limit_strategy=TrafficLimitStrategy.MONTH, telegram_id=target_user.telegram_id, + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=target_user.full_name, username=target_user.username, telegram_id=target_user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - create_kwargs['hwid_device_limit'] = hwid_limit - - remnawave_user = await api.create_user(**create_kwargs) if remnawave_user and hasattr(remnawave_user, 'uuid'): target_user.remnawave_uuid = remnawave_user.uuid diff --git a/app/handlers/simple_subscription.py b/app/handlers/simple_subscription.py index f839949f..43051911 100644 --- a/app/handlers/simple_subscription.py +++ b/app/handlers/simple_subscription.py @@ -111,32 +111,20 @@ async def start_simple_subscription_purchase( subscription_params, resolved_squad_uuid, ) - show_devices = settings.is_devices_selection_enabled() - - message_lines = [ - "⚡ Простая покупка подписки", - "", - f"📅 Период: {subscription_params['period_days']} дней", - ] - - if show_devices: - message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") - - message_lines.extend([ - f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}", - f"🌍 Сервер: {server_label}", - "", - f"💰 Стоимость: {settings.format_price(price_kopeks)}", - f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}", - "", - ( + message_text = ( + f"⚡ Простая покупка подписки\n\n" + f"📅 Период: {subscription_params['period_days']} дней\n" + f"📱 Устройства: {subscription_params['device_limit']}\n" + f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n" + f"🌍 Сервер: {server_label}\n\n" + f"💰 Стоимость: {settings.format_price(price_kopeks)}\n" + f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}\n\n" + + ( "Вы можете оплатить подписку с баланса или выбрать другой способ оплаты." if can_pay_from_balance else "Баланс пока недостаточный для мгновенной оплаты. Выберите подходящий способ оплаты:" - ), - ]) - - message_text = "\n".join(message_lines) + ) + ) if trial_notice: message_text = f"{trial_notice}\n\n{message_text}" @@ -445,28 +433,16 @@ async def handle_simple_subscription_pay_with_balance( subscription_params, resolved_squad_uuid, ) - show_devices = settings.is_devices_selection_enabled() - - success_lines = [ - "✅ Подписка успешно активирована!", - "", - f"📅 Период: {subscription_params['period_days']} дней", - ] - - if show_devices: - success_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") - - success_lines.extend([ - f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}", - f"🌍 Сервер: {server_label}", - "", - f"💰 Списано с баланса: {settings.format_price(price_kopeks)}", - f"💳 Ваш баланс: {settings.format_price(db_user.balance_kopeks)}", - "", - "🔗 Для подключения перейдите в раздел 'Подключиться'", - ]) - - success_message = "\n".join(success_lines) + success_message = ( + f"✅ Подписка успешно активирована!\n\n" + f"📅 Период: {subscription_params['period_days']} дней\n" + f"📱 Устройства: {subscription_params['device_limit']}\n" + f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n" + f"🌍 Сервер: {server_label}\n\n" + f"💰 Списано с баланса: {settings.format_price(price_kopeks)}\n" + f"💳 Ваш баланс: {settings.format_price(db_user.balance_kopeks)}\n\n" + f"🔗 Для подключения перейдите в раздел 'Подключиться'" + ) connect_mode = settings.CONNECT_BUTTON_MODE subscription_link = get_display_subscription_link(subscription) @@ -643,31 +619,19 @@ async def handle_simple_subscription_other_payment_methods( subscription_params, resolved_squad_uuid, ) - show_devices = settings.is_devices_selection_enabled() - - message_lines = [ - "💳 Оплата подписки", - "", - f"📅 Период: {subscription_params['period_days']} дней", - ] - - if show_devices: - message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") - - message_lines.extend([ - f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}", - f"🌍 Сервер: {server_label}", - "", - f"💰 Стоимость: {settings.format_price(price_kopeks)}", - "", - ( + message_text = ( + f"💳 Оплата подписки\n\n" + f"📅 Период: {subscription_params['period_days']} дней\n" + f"📱 Устройства: {subscription_params['device_limit']}\n" + f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n" + f"🌍 Сервер: {server_label}\n\n" + f"💰 Стоимость: {settings.format_price(price_kopeks)}\n\n" + + ( "Вы можете оплатить подписку с баланса или выбрать другой способ оплаты:" if can_pay_from_balance else "Выберите подходящий способ оплаты:" - ), - ]) - - message_text = "\n".join(message_lines) + ) + ) base_keyboard = _get_simple_subscription_payment_keyboard(db_user.language) keyboard_rows = [] @@ -890,25 +854,14 @@ async def handle_simple_subscription_payment_method( keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_buttons) # Подготавливаем текст сообщения - show_devices = settings.is_devices_selection_enabled() - - message_lines = [ - "💳 Оплата подписки через YooKassa", - "", - f"📅 Период: {subscription_params['period_days']} дней", - ] - - if show_devices: - message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") - - message_lines.extend([ - f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}", - f"💰 Сумма: {settings.format_price(price_kopeks)}", - f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...", - "", - ]) - - message_text = "\n".join(message_lines) + message_text = ( + f"💳 Оплата подписки через YooKassa\n\n" + f"📅 Период: {subscription_params['period_days']} дней\n" + f"📱 Устройства: {subscription_params['device_limit']}\n" + f"📊 Трафик: {'Безлимит' if subscription_params['traffic_limit_gb'] == 0 else f'{subscription_params['traffic_limit_gb']} ГБ'}\n" + f"💰 Сумма: {settings.format_price(price_kopeks)}\n" + f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" + ) # Добавляем инструкции в зависимости от доступных способов оплаты if not confirmation_url: diff --git a/app/handlers/subscription/autopay.py b/app/handlers/subscription/autopay.py index 0ae8897c..7921bb1f 100644 --- a/app/handlers/subscription/autopay.py +++ b/app/handlers/subscription/autopay.py @@ -208,20 +208,39 @@ async def handle_subscription_config_back( await state.set_state(SubscriptionStates.selecting_period) elif current_state == SubscriptionStates.selecting_devices.state: - await _show_previous_configuration_step(callback, state, db_user, texts, db) - - elif current_state == SubscriptionStates.confirming_purchase.state: - if settings.is_devices_selection_enabled(): + if await _should_show_countries_management(db_user): + countries = await _get_available_countries(db_user.promo_group_id) data = await state.get_data() - selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) + selected_countries = data.get('countries', []) await callback.message.edit_text( - texts.SELECT_DEVICES, - reply_markup=get_devices_keyboard(selected_devices, db_user.language) + texts.SELECT_COUNTRIES, + reply_markup=get_countries_keyboard(countries, selected_countries, db_user.language) ) - await state.set_state(SubscriptionStates.selecting_devices) + await state.set_state(SubscriptionStates.selecting_countries) + elif settings.is_traffic_selectable(): + await callback.message.edit_text( + texts.SELECT_TRAFFIC, + reply_markup=get_traffic_packages_keyboard(db_user.language) + ) + await state.set_state(SubscriptionStates.selecting_traffic) else: - await _show_previous_configuration_step(callback, state, db_user, texts, db) + await callback.message.edit_text( + await _build_subscription_period_prompt(db_user, texts, db), + reply_markup=get_subscription_period_keyboard(db_user.language), + parse_mode="HTML", + ) + await state.set_state(SubscriptionStates.selecting_period) + + elif current_state == SubscriptionStates.confirming_purchase.state: + data = await state.get_data() + selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) + + await callback.message.edit_text( + texts.SELECT_DEVICES, + reply_markup=get_devices_keyboard(selected_devices, db_user.language) + ) + await state.set_state(SubscriptionStates.selecting_devices) else: from app.handlers.menu import show_main_menu @@ -248,37 +267,3 @@ async def handle_subscription_cancel( await show_main_menu(callback, db_user, db) await callback.answer("❌ Покупка отменена") -async def _show_previous_configuration_step( - callback: types.CallbackQuery, - state: FSMContext, - db_user: User, - texts, - db: AsyncSession, -): - if await _should_show_countries_management(db_user): - countries = await _get_available_countries(db_user.promo_group_id) - data = await state.get_data() - selected_countries = data.get('countries', []) - - await callback.message.edit_text( - texts.SELECT_COUNTRIES, - reply_markup=get_countries_keyboard(countries, selected_countries, db_user.language) - ) - await state.set_state(SubscriptionStates.selecting_countries) - return - - if settings.is_traffic_selectable(): - await callback.message.edit_text( - texts.SELECT_TRAFFIC, - reply_markup=get_traffic_packages_keyboard(db_user.language) - ) - await state.set_state(SubscriptionStates.selecting_traffic) - return - - await callback.message.edit_text( - await _build_subscription_period_prompt(db_user, texts, db), - reply_markup=get_subscription_period_keyboard(db_user.language), - parse_mode="HTML", - ) - await state.set_state(SubscriptionStates.selecting_period) - diff --git a/app/handlers/subscription/countries.py b/app/handlers/subscription/countries.py index 997ef357..1e964189 100644 --- a/app/handlers/subscription/countries.py +++ b/app/handlers/subscription/countries.py @@ -79,7 +79,6 @@ from app.utils.promo_offer import ( ) from .common import _get_addon_discount_percent_for_user, _get_period_hint_from_subscription, logger -from .summary import present_subscription_summary async def handle_add_countries( callback: types.CallbackQuery, @@ -589,11 +588,6 @@ async def countries_continue( await callback.answer("⚠️ Выберите хотя бы одну страну!", show_alert=True) return - if not settings.is_devices_selection_enabled(): - if await present_subscription_summary(callback, state, db_user, texts): - await callback.answer() - return - selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) await callback.message.edit_text( diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py index cecfdf22..3ca44442 100644 --- a/app/handlers/subscription/devices.py +++ b/app/handlers/subscription/devices.py @@ -183,13 +183,6 @@ async def handle_change_devices( texts = get_texts(db_user.language) subscription = db_user.subscription - if not settings.is_devices_selection_enabled(): - await callback.answer( - texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"), - show_alert=True, - ) - return - if not subscription or subscription.is_trial: await callback.answer( texts.t("PAID_FEATURE_ONLY", "⚠️ Эта функция доступна только для платных подписок"), @@ -240,13 +233,6 @@ async def confirm_change_devices( texts = get_texts(db_user.language) subscription = db_user.subscription - if not settings.is_devices_selection_enabled(): - await callback.answer( - texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"), - show_alert=True, - ) - return - current_devices = subscription.device_limit if new_devices_count == current_devices: @@ -393,13 +379,6 @@ async def execute_change_devices( subscription = db_user.subscription current_devices = subscription.device_limit - if not settings.is_devices_selection_enabled(): - await callback.answer( - texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"), - show_alert=True, - ) - return - try: if price > 0: success = await subtract_user_balance( @@ -884,13 +863,6 @@ async def confirm_add_devices( texts = get_texts(db_user.language) subscription = db_user.subscription - if not settings.is_devices_selection_enabled(): - await callback.answer( - texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Изменение количества устройств недоступно"), - show_alert=True, - ) - return - resume_callback = None new_total_devices = subscription.device_limit + devices_count diff --git a/app/handlers/subscription/pricing.py b/app/handlers/subscription/pricing.py index 0016e4f3..b1a589f4 100644 --- a/app/handlers/subscription/pricing.py +++ b/app/handlers/subscription/pricing.py @@ -159,12 +159,6 @@ async def _prepare_subscription_summary( selected_server_prices.append(total_price_for_server) devices_selected = summary_data.get('devices', settings.DEFAULT_DEVICE_LIMIT) - devices_selection_enabled = settings.is_devices_selection_enabled() - - if not devices_selection_enabled: - devices_selected = settings.DEFAULT_DEVICE_LIMIT - - summary_data['devices'] = devices_selected additional_devices = max(0, devices_selected - settings.DEFAULT_DEVICE_LIMIT) devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE devices_discount_percent = db_user.get_promo_discount( @@ -281,7 +275,7 @@ async def _prepare_subscription_summary( f" -{texts.format_price(total_servers_discount)})" ) details_lines.append(servers_line) - if devices_selection_enabled and total_devices_price > 0: + if total_devices_price > 0: devices_line = ( f"- Доп. устройства: {texts.format_price(devices_price_per_month)}/мес × {months_in_period}" f" = {texts.format_price(total_devices_price)}" @@ -306,28 +300,17 @@ async def _prepare_subscription_summary( details_text = "\n".join(details_lines) - summary_lines = [ - "📋 Сводка заказа", - "", - f"📅 Период: {period_display}", - f"📊 Трафик: {traffic_display}", - f"🌍 Страны: {', '.join(selected_countries_names)}", - ] - - if devices_selection_enabled: - summary_lines.append(f"📱 Устройства: {devices_selected}") - - summary_lines.extend([ - "", - "💰 Детализация стоимости:", - details_text, - "", - f"💎 Общая стоимость: {texts.format_price(total_price)}", - "", - "Подтверждаете покупку?", - ]) - - summary_text = "\n".join(summary_lines) + summary_text = ( + "📋 Сводка заказа\n\n" + f"📅 Период: {period_display}\n" + f"📊 Трафик: {traffic_display}\n" + f"🌍 Страны: {', '.join(selected_countries_names)}\n" + f"📱 Устройства: {devices_selected}\n\n" + "💰 Детализация стоимости:\n" + f"{details_text}\n\n" + f"💎 Общая стоимость: {texts.format_price(total_price)}\n\n" + "Подтверждаете покупку?" + ) return summary_text, summary_data @@ -427,12 +410,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int: return 0 async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession): - devices_selection_enabled = settings.is_devices_selection_enabled() - - if devices_selection_enabled: - devices_used = await get_current_devices_count(db_user) - else: - devices_used = 0 + devices_used = await get_current_devices_count(db_user) countries_info = await _get_countries_info(subscription.connected_squads) countries_text = ", ".join([c['name'] for c in countries_info]) if countries_info else "Нет" @@ -461,18 +439,7 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess subscription_cost = await get_subscription_cost(subscription, db) - info_template = texts.SUBSCRIPTION_INFO - - if not devices_selection_enabled: - info_template = info_template.replace( - "\n📱 Устройства: {devices_used} / {devices_limit}", - "", - ).replace( - "\n📱 Devices: {devices_used} / {devices_limit}", - "", - ) - - info_text = info_template.format( + info_text = texts.SUBSCRIPTION_INFO.format( status=status_text, type=type_text, end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"), diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 96dc77be..d9f8bbcc 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -140,7 +140,6 @@ from .traffic import ( handle_switch_traffic, select_traffic, ) -from .summary import present_subscription_summary async def show_subscription_info( callback: types.CallbackQuery, @@ -238,32 +237,26 @@ async def show_subscription_info( devices_list = [] devices_count = 0 - show_devices = settings.is_devices_selection_enabled() - devices_used_str = "" - devices_list: List[Dict[str, Any]] = [] + try: + if db_user.remnawave_uuid: + from app.services.remnawave_service import RemnaWaveService + service = RemnaWaveService() - if show_devices: - try: - if db_user.remnawave_uuid: - from app.services.remnawave_service import RemnaWaveService - service = RemnaWaveService() + async with service.get_api_client() as api: + response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}') - async with service.get_api_client() as api: - response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}') + if response and 'response' in response: + devices_info = response['response'] + devices_count = devices_info.get('total', 0) + devices_list = devices_info.get('devices', []) + devices_used_str = str(devices_count) + logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}") + else: + logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}") - if response and 'response' in response: - devices_info = response['response'] - devices_count = devices_info.get('total', 0) - devices_list = devices_info.get('devices', []) - devices_used_str = str(devices_count) - logger.info(f"Найдено {devices_count} устройств для пользователя {db_user.telegram_id}") - else: - logger.warning(f"Не удалось получить информацию об устройствах для {db_user.telegram_id}") - - except Exception as e: - logger.error(f"Ошибка получения устройств для отображения: {e}") - devices_used = await get_current_devices_count(db_user) - devices_used_str = str(devices_used) + except Exception as e: + logger.error(f"Ошибка получения устройств для отображения: {e}") + devices_used_str = await get_current_devices_count(db_user) servers_names = await get_servers_display_names(subscription.connected_squads) servers_display = ( @@ -272,7 +265,7 @@ async def show_subscription_info( else texts.t("SUBSCRIPTION_NO_SERVERS", "Нет серверов") ) - message_template = texts.t( + message = texts.t( "SUBSCRIPTION_OVERVIEW_TEMPLATE", """👤 {full_name} 💰 Баланс: {balance} @@ -285,15 +278,7 @@ async def show_subscription_info( 📈 Трафик: {traffic} 🌍 Серверы: {servers} 📱 Устройства: {devices_used} / {device_limit}""", - ) - - if not show_devices: - message_template = message_template.replace( - "\n📱 Устройства: {devices_used} / {device_limit}", - "", - ) - - message = message_template.format( + ).format( full_name=db_user.full_name, balance=settings.format_price(db_user.balance_kopeks), status_emoji=status_emoji, @@ -308,7 +293,7 @@ async def show_subscription_info( device_limit=subscription.device_limit, ) - if show_devices and devices_list: + if devices_list and len(devices_list) > 0: message += "\n\n" + texts.t( "SUBSCRIPTION_CONNECTED_DEVICES_TITLE", "
📱 Подключенные устройства:\n", @@ -713,61 +698,7 @@ async def return_to_saved_cart( return texts = get_texts(db_user.language) - - preserved_metadata_keys = { - 'saved_cart', - 'missing_amount', - 'return_to_cart', - 'user_id', - } - preserved_metadata = { - key: cart_data[key] - for key in preserved_metadata_keys - if key in cart_data - } - - prepared_cart_data = dict(cart_data) - - if not settings.is_devices_selection_enabled(): - try: - from .pricing import _prepare_subscription_summary - - _, recalculated_data = await _prepare_subscription_summary( - db_user, - prepared_cart_data, - texts, - ) - except ValueError as recalculation_error: - logger.error( - "Не удалось пересчитать сохраненную корзину пользователя %s: %s", - db_user.telegram_id, - recalculation_error, - ) - default_limit = max(getattr(settings, "DEFAULT_DEVICE_LIMIT", 1), 1) - prepared_cart_data['devices'] = default_limit - removed_devices_total = prepared_cart_data.pop('total_devices_price', 0) or 0 - if removed_devices_total: - prepared_cart_data['total_price'] = max( - 0, - prepared_cart_data.get('total_price', 0) - removed_devices_total, - ) - prepared_cart_data.pop('devices_discount_percent', None) - prepared_cart_data.pop('devices_discount_total', None) - prepared_cart_data.pop('devices_discounted_price_per_month', None) - prepared_cart_data.pop('devices_price_per_month', None) - else: - normalized_cart_data = dict(prepared_cart_data) - normalized_cart_data.update(recalculated_data) - - for key, value in preserved_metadata.items(): - normalized_cart_data[key] = value - - prepared_cart_data = normalized_cart_data - - if prepared_cart_data != cart_data: - await user_cart_service.save_user_cart(db_user.id, prepared_cart_data) - - total_price = prepared_cart_data.get('total_price', 0) + total_price = cart_data.get('total_price', 0) if db_user.balance_kopeks < total_price: missing_amount = total_price - db_user.balance_kopeks @@ -786,45 +717,30 @@ async def return_to_saved_cart( countries = await _get_available_countries(db_user.promo_group_id) selected_countries_names = [] - period_display = format_period_description(prepared_cart_data['period_days'], db_user.language) + months_in_period = calculate_months_from_days(cart_data['period_days']) + period_display = format_period_description(cart_data['period_days'], db_user.language) for country in countries: - if country['uuid'] in prepared_cart_data['countries']: + if country['uuid'] in cart_data['countries']: selected_countries_names.append(country['name']) if settings.is_traffic_fixed(): - traffic_value = prepared_cart_data.get('traffic_gb') - if traffic_value is None: - traffic_value = settings.get_fixed_traffic_limit() - traffic_display = "Безлимитный" if traffic_value == 0 else f"{traffic_value} ГБ" + traffic_display = "Безлимитный" if cart_data['traffic_gb'] == 0 else f"{cart_data['traffic_gb']} ГБ" else: - traffic_value = prepared_cart_data.get('traffic_gb', 0) or 0 - traffic_display = "Безлимитный" if traffic_value == 0 else f"{traffic_value} ГБ" + traffic_display = "Безлимитный" if cart_data['traffic_gb'] == 0 else f"{cart_data['traffic_gb']} ГБ" - summary_lines = [ - "🛒 Восстановленная корзина", - "", - f"📅 Период: {period_display}", - f"📊 Трафик: {traffic_display}", - f"🌍 Страны: {', '.join(selected_countries_names)}", - ] - - if settings.is_devices_selection_enabled(): - devices_value = prepared_cart_data.get('devices') - if devices_value is not None: - summary_lines.append(f"📱 Устройства: {devices_value}") - - summary_lines.extend([ - "", - f"💎 Общая стоимость: {texts.format_price(total_price)}", - "", - "Подтверждаете покупку?", - ]) - - summary_text = "\n".join(summary_lines) + summary_text = ( + "🛒 Восстановленная корзина\n\n" + f"📅 Период: {period_display}\n" + f"📊 Трафик: {traffic_display}\n" + f"🌍 Страны: {', '.join(selected_countries_names)}\n" + f"📱 Устройства: {cart_data['devices']}\n\n" + f"💎 Общая стоимость: {texts.format_price(total_price)}\n\n" + "Подтверждаете покупку?" + ) # Устанавливаем данные в FSM для продолжения процесса - await state.set_data(prepared_cart_data) + await state.set_data(cart_data) await state.set_state(SubscriptionStates.confirming_purchase) await callback.message.edit_text( @@ -956,27 +872,16 @@ async def handle_extend_subscription( texts=texts, ) - renewal_lines = [ - "⏰ Продление подписки", - "", - f"Осталось дней: {subscription.days_left}", - "", - "Ваша текущая конфигурация:", - f"🌍 Серверов: {len(subscription.connected_squads)}", - f"📊 Трафик: {texts.format_traffic(subscription.traffic_limit_gb)}", - ] - - if settings.is_devices_selection_enabled(): - renewal_lines.append(f"📱 Устройств: {subscription.device_limit}") - - renewal_lines.extend([ - "", - "Выберите период продления:", - prices_text.rstrip(), - "", - ]) - - message_text = "\n".join(renewal_lines) + message_text = ( + "⏰ Продление подписки\n\n" + f"Осталось дней: {subscription.days_left}\n\n" + f"Ваша текущая конфигурация:\n" + f"🌍 Серверов: {len(subscription.connected_squads)}\n" + f"📊 Трафик: {texts.format_traffic(subscription.traffic_limit_gb)}\n" + f"📱 Устройств: {subscription.device_limit}\n\n" + f"Выберите период продления:\n" + f"{prices_text.rstrip()}\n\n" + ) if promo_discounts_text: message_text += f"{promo_discounts_text}\n\n" @@ -1343,60 +1248,43 @@ async def select_period( reply_markup=get_traffic_packages_keyboard(db_user.language) ) await state.set_state(SubscriptionStates.selecting_traffic) - await callback.answer() - return + else: + if await _should_show_countries_management(db_user): + countries = await _get_available_countries(db_user.promo_group_id) + await callback.message.edit_text( + texts.SELECT_COUNTRIES, + reply_markup=get_countries_keyboard(countries, [], db_user.language) + ) + await state.set_state(SubscriptionStates.selecting_countries) + else: + countries = await _get_available_countries(db_user.promo_group_id) + available_countries = [c for c in countries if c.get('is_available', True)] + data['countries'] = [available_countries[0]['uuid']] if available_countries else [] + await state.set_data(data) - if await _should_show_countries_management(db_user): - countries = await _get_available_countries(db_user.promo_group_id) - await callback.message.edit_text( - texts.SELECT_COUNTRIES, - reply_markup=get_countries_keyboard(countries, [], db_user.language) - ) - await state.set_state(SubscriptionStates.selecting_countries) - await callback.answer() - return + selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) - countries = await _get_available_countries(db_user.promo_group_id) - available_countries = [c for c in countries if c.get('is_available', True)] - data['countries'] = [available_countries[0]['uuid']] if available_countries else [] - await state.set_data(data) + await callback.message.edit_text( + texts.SELECT_DEVICES, + reply_markup=get_devices_keyboard(selected_devices, db_user.language) + ) + await state.set_state(SubscriptionStates.selecting_devices) - if settings.is_devices_selection_enabled(): - selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) - - await callback.message.edit_text( - texts.SELECT_DEVICES, - reply_markup=get_devices_keyboard(selected_devices, db_user.language) - ) - await state.set_state(SubscriptionStates.selecting_devices) - await callback.answer() - return - - if await present_subscription_summary(callback, state, db_user, texts): - await callback.answer() + await callback.answer() async def select_devices( callback: types.CallbackQuery, state: FSMContext, db_user: User ): - texts = get_texts(db_user.language) - - if not settings.is_devices_selection_enabled(): - await callback.answer( - texts.t("DEVICES_SELECTION_DISABLED", "⚠️ Выбор количества устройств недоступен"), - show_alert=True, - ) - return - if not callback.data.startswith("devices_") or callback.data == "devices_continue": - await callback.answer(texts.t("DEVICES_INVALID_REQUEST", "❌ Некорректный запрос"), show_alert=True) + await callback.answer("❌ Некорректный запрос", show_alert=True) return try: devices = int(callback.data.split('_')[1]) except (ValueError, IndexError): - await callback.answer(texts.t("DEVICES_INVALID_COUNT", "❌ Некорректное количество устройств"), show_alert=True) + await callback.answer("❌ Некорректное количество устройств", show_alert=True) return data = await state.get_data() @@ -1433,8 +1321,27 @@ async def devices_continue( await callback.answer("⚠️ Некорректный запрос", show_alert=True) return - if await present_subscription_summary(callback, state, db_user): - await callback.answer() + data = await state.get_data() + texts = get_texts(db_user.language) + + try: + summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts) + except ValueError: + logger.error(f"Ошибка в расчете цены подписки для пользователя {db_user.telegram_id}") + await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True) + return + + await state.set_data(prepared_data) + await save_subscription_checkout_draft(db_user.id, prepared_data) + + await callback.message.edit_text( + summary_text, + reply_markup=get_subscription_confirm_keyboard(db_user.language), + parse_mode="HTML", + ) + + await state.set_state(SubscriptionStates.confirming_purchase) + await callback.answer() async def confirm_purchase( callback: types.CallbackQuery, @@ -1529,43 +1436,30 @@ async def confirm_purchase( total_servers_discount = data.get('servers_discount_total', 0) servers_discount_percent = data.get('servers_discount_percent', 0) - devices_selection_enabled = settings.is_devices_selection_enabled() - devices_selected = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) - - if not devices_selection_enabled: - devices_selected = settings.DEFAULT_DEVICE_LIMIT - - additional_devices = max(0, devices_selected - settings.DEFAULT_DEVICE_LIMIT) + additional_devices = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) devices_price_per_month = data.get( 'devices_price_per_month', additional_devices * settings.PRICE_PER_DEVICE ) - - devices_discount_percent = 0 - discounted_devices_price_per_month = 0 - devices_discount_total = 0 - total_devices_price = 0 - - if devices_selection_enabled and additional_devices > 0: - if 'devices_discount_percent' in data: - devices_discount_percent = data.get('devices_discount_percent', 0) - discounted_devices_price_per_month = data.get( - 'devices_discounted_price_per_month', devices_price_per_month - ) - devices_discount_total = data.get('devices_discount_total', 0) - total_devices_price = data.get( - 'total_devices_price', discounted_devices_price_per_month * months_in_period - ) - else: - devices_discount_percent = db_user.get_promo_discount( - "devices", - data['period_days'], - ) - discounted_devices_price_per_month, discount_per_month = apply_percentage_discount( - devices_price_per_month, - devices_discount_percent, - ) - devices_discount_total = discount_per_month * months_in_period - total_devices_price = discounted_devices_price_per_month * months_in_period + if 'devices_discount_percent' in data: + devices_discount_percent = data.get('devices_discount_percent', 0) + discounted_devices_price_per_month = data.get( + 'devices_discounted_price_per_month', devices_price_per_month + ) + devices_discount_total = data.get('devices_discount_total', 0) + total_devices_price = data.get( + 'total_devices_price', discounted_devices_price_per_month * months_in_period + ) + else: + devices_discount_percent = db_user.get_promo_discount( + "devices", + data['period_days'], + ) + discounted_devices_price_per_month, discount_per_month = apply_percentage_discount( + devices_price_per_month, + devices_discount_percent, + ) + devices_discount_total = discount_per_month * months_in_period + total_devices_price = discounted_devices_price_per_month * months_in_period if settings.is_traffic_fixed(): final_traffic_gb = settings.get_fixed_traffic_limit() @@ -1772,15 +1666,6 @@ async def confirm_purchase( return existing_subscription = db_user.subscription - if settings.is_devices_selection_enabled(): - selected_devices = devices_selected - else: - selected_devices = settings.DEFAULT_DEVICE_LIMIT - - should_update_devices = ( - settings.is_devices_selection_enabled() and selected_devices is not None - ) - was_trial_conversion = False current_time = datetime.utcnow() @@ -1823,8 +1708,7 @@ async def confirm_purchase( existing_subscription.is_trial = False existing_subscription.status = SubscriptionStatus.ACTIVE.value existing_subscription.traffic_limit_gb = final_traffic_gb - if should_update_devices: - existing_subscription.device_limit = selected_devices + existing_subscription.device_limit = data['devices'] existing_subscription.connected_squads = data['countries'] existing_subscription.start_date = current_time @@ -1839,24 +1723,11 @@ async def confirm_purchase( else: logger.info(f"Создаем новую подписку для пользователя {db_user.telegram_id}") - default_device_limit = getattr(settings, "DEFAULT_DEVICE_LIMIT", 1) - if should_update_devices: - resolved_device_limit = selected_devices - else: - resolved_device_limit = ( - selected_devices - if selected_devices is not None - else default_device_limit - ) - - if resolved_device_limit is None: - resolved_device_limit = default_device_limit - subscription = await create_paid_subscription_with_traffic_mode( db=db, user_id=db_user.id, duration_days=data['period_days'], - device_limit=resolved_device_limit, + device_limit=data['devices'], connected_squads=data['countries'], traffic_gb=final_traffic_gb ) @@ -1874,11 +1745,11 @@ async def confirm_purchase( await add_user_to_servers(db, server_ids) logger.info(f"Сохранены цены серверов за весь период: {server_prices}") - + await db.refresh(db_user) - + subscription_service = SubscriptionService() - + if db_user.remnawave_uuid: remnawave_user = await subscription_service.update_remnawave_user( db, @@ -1893,7 +1764,7 @@ async def confirm_purchase( reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, reset_reason="покупка подписки", ) - + if not remnawave_user: logger.error(f"Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}") remnawave_user = await subscription_service.create_remnawave_user( @@ -1902,7 +1773,7 @@ async def confirm_purchase( reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, reset_reason="покупка подписки (повторная попытка)", ) - + transaction = await create_transaction( db=db, user_id=db_user.id, @@ -1910,7 +1781,7 @@ async def confirm_purchase( amount_kopeks=final_price, description=f"Подписка на {data['period_days']} дней ({months_in_period} мес)" ) - + try: notification_service = AdminNotificationService(callback.bot) await notification_service.send_subscription_purchase_notification( @@ -2117,7 +1988,7 @@ async def create_paid_subscription_with_traffic_mode( db: AsyncSession, user_id: int, duration_days: int, - device_limit: Optional[int], + device_limit: int, connected_squads: List[str], traffic_gb: Optional[int] = None ): @@ -2131,20 +2002,16 @@ async def create_paid_subscription_with_traffic_mode( else: traffic_limit_gb = traffic_gb - create_kwargs = dict( + subscription = await create_paid_subscription( db=db, user_id=user_id, duration_days=duration_days, traffic_limit_gb=traffic_limit_gb, + device_limit=device_limit, connected_squads=connected_squads, update_server_counters=False, ) - if device_limit is not None: - create_kwargs['device_limit'] = device_limit - - subscription = await create_paid_subscription(**create_kwargs) - logger.info(f"📋 Создана подписка с трафиком: {traffic_limit_gb} ГБ (режим: {settings.TRAFFIC_SELECTION_MODE})") return subscription @@ -2167,14 +2034,9 @@ async def handle_subscription_settings( ) return - show_devices = settings.is_devices_selection_enabled() + devices_used = await get_current_devices_count(db_user) - if show_devices: - devices_used = await get_current_devices_count(db_user) - else: - devices_used = 0 - - settings_template = texts.t( + settings_text = texts.t( "SUBSCRIPTION_SETTINGS_OVERVIEW", ( "⚙️ Настройки подписки\n\n" @@ -2184,15 +2046,7 @@ async def handle_subscription_settings( "📱 Устройства: {devices_used} / {devices_limit}\n\n" "Выберите что хотите изменить:" ), - ) - - if not show_devices: - settings_template = settings_template.replace( - "\n📱 Устройства: {devices_used} / {devices_limit}", - "", - ) - - settings_text = settings_template.format( + ).format( countries_count=len(subscription.connected_squads), traffic_used=texts.format_traffic(subscription.traffic_used_gb), traffic_limit=texts.format_traffic(subscription.traffic_limit_gb), @@ -2587,26 +2441,16 @@ async def handle_simple_subscription_purchase( if user_balance_kopeks >= price_kopeks: # Если баланс достаточный, предлагаем оплатить с баланса - simple_lines = [ - "⚡ Простая покупка подписки", - "", - f"📅 Период: {subscription_params['period_days']} дней", - ] - - if settings.is_devices_selection_enabled(): - simple_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") - - simple_lines.extend([ - f"📊 Трафик: {traffic_text}", - f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}", - "", - f"💰 Стоимость: {settings.format_price(price_kopeks)}", - f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}", - "", - "Вы можете оплатить подписку с баланса или выбрать другой способ оплаты.", - ]) - - message_text = "\n".join(simple_lines) + message_text = ( + f"⚡ Простая покупка подписки\n\n" + f"📅 Период: {subscription_params['period_days']} дней\n" + f"📱 Устройства: {subscription_params['device_limit']}\n" + f"📊 Трафик: {traffic_text}\n" + f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}\n\n" + f"💰 Стоимость: {settings.format_price(price_kopeks)}\n" + f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}\n\n" + f"Вы можете оплатить подписку с баланса или выбрать другой способ оплаты." + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="✅ Оплатить с баланса", callback_data="simple_subscription_pay_with_balance")], @@ -2615,26 +2459,16 @@ async def handle_simple_subscription_purchase( ]) else: # Если баланс недостаточный, предлагаем внешние способы оплаты - simple_lines = [ - "⚡ Простая покупка подписки", - "", - f"📅 Период: {subscription_params['period_days']} дней", - ] - - if settings.is_devices_selection_enabled(): - simple_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") - - simple_lines.extend([ - f"📊 Трафик: {traffic_text}", - f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}", - "", - f"💰 Стоимость: {settings.format_price(price_kopeks)}", - f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}", - "", - "Выберите способ оплаты:", - ]) - - message_text = "\n".join(simple_lines) + message_text = ( + f"⚡ Простая покупка подписки\n\n" + f"📅 Период: {subscription_params['period_days']} дней\n" + f"📱 Устройства: {subscription_params['device_limit']}\n" + f"📊 Трафик: {traffic_text}\n" + f"🌍 Сервер: {'Любой доступный' if not subscription_params['squad_uuid'] else 'Выбранный'}\n\n" + f"💰 Стоимость: {settings.format_price(price_kopeks)}\n" + f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}\n\n" + f"Выберите способ оплаты:" + ) keyboard = _get_simple_subscription_payment_keyboard(db_user.language) diff --git a/app/handlers/subscription/summary.py b/app/handlers/subscription/summary.py deleted file mode 100644 index ab65b0f9..00000000 --- a/app/handlers/subscription/summary.py +++ /dev/null @@ -1,59 +0,0 @@ -import logging -from typing import Optional, TYPE_CHECKING - -from aiogram import types -from aiogram.fsm.context import FSMContext - -from app.localization.texts import get_texts -from app.services.subscription_checkout_service import save_subscription_checkout_draft -from app.states import SubscriptionStates -from app.keyboards.inline import get_subscription_confirm_keyboard - -if TYPE_CHECKING: # pragma: no cover - only for type checking - from .pricing import _prepare_subscription_summary - - -logger = logging.getLogger(__name__) - - -async def present_subscription_summary( - callback: types.CallbackQuery, - state: FSMContext, - db_user, - texts: Optional = None, -) -> bool: - """Render the subscription purchase summary and switch to the confirmation state. - - Returns ``True`` when the summary is shown successfully and ``False`` if - calculation failed (an error is shown to the user in this case). - """ - - if texts is None: - texts = get_texts(db_user.language) - - data = await state.get_data() - - from .pricing import _prepare_subscription_summary - - try: - summary_text, prepared_data = await _prepare_subscription_summary(db_user, data, texts) - except ValueError as exc: - logger.error( - "Ошибка в расчете цены подписки для пользователя %s: %s", - db_user.telegram_id, - exc, - ) - await callback.answer("Ошибка расчета цены. Обратитесь в поддержку.", show_alert=True) - return False - - await state.set_data(prepared_data) - await save_subscription_checkout_draft(db_user.id, prepared_data) - - await callback.message.edit_text( - summary_text, - reply_markup=get_subscription_confirm_keyboard(db_user.language), - parse_mode="HTML", - ) - - await state.set_state(SubscriptionStates.confirming_purchase) - return True diff --git a/app/handlers/subscription/traffic.py b/app/handlers/subscription/traffic.py index b86d85bf..6733db4b 100644 --- a/app/handlers/subscription/traffic.py +++ b/app/handlers/subscription/traffic.py @@ -80,7 +80,6 @@ from app.utils.promo_offer import ( from .common import _apply_addon_discount, _get_addon_discount_percent_for_user, _get_period_hint_from_subscription, get_confirm_switch_traffic_keyboard, get_traffic_switch_keyboard, logger from .countries import _get_available_countries, _should_show_countries_management -from .summary import present_subscription_summary async def handle_add_traffic( callback: types.CallbackQuery, @@ -353,15 +352,12 @@ async def select_traffic( reply_markup=get_countries_keyboard(countries, [], db_user.language) ) await state.set_state(SubscriptionStates.selecting_countries) - await callback.answer() - return + else: + countries = await _get_available_countries(db_user.promo_group_id) + available_countries = [c for c in countries if c.get('is_available', True)] + data['countries'] = [available_countries[0]['uuid']] if available_countries else [] + await state.set_data(data) - countries = await _get_available_countries(db_user.promo_group_id) - available_countries = [c for c in countries if c.get('is_available', True)] - data['countries'] = [available_countries[0]['uuid']] if available_countries else [] - await state.set_data(data) - - if settings.is_devices_selection_enabled(): selected_devices = data.get('devices', settings.DEFAULT_DEVICE_LIMIT) await callback.message.edit_text( @@ -369,11 +365,8 @@ async def select_traffic( reply_markup=get_devices_keyboard(selected_devices, db_user.language) ) await state.set_state(SubscriptionStates.selecting_devices) - await callback.answer() - return - if await present_subscription_summary(callback, state, db_user, texts): - await callback.answer() + await callback.answer() async def add_traffic( callback: types.CallbackQuery, diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 19624cf7..2656392f 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -2085,38 +2085,33 @@ def get_updated_subscription_settings_keyboard(language: str = DEFAULT_LANGUAGE, texts = get_texts(language) keyboard = [] - + if show_countries_management: keyboard.append([ InlineKeyboardButton(text=texts.t("ADD_COUNTRIES_BUTTON", "🌐 Добавить страны"), callback_data="subscription_add_countries") ]) + keyboard.extend([ + [ + InlineKeyboardButton(text=texts.t("CHANGE_DEVICES_BUTTON", "📱 Изменить устройства"), callback_data="subscription_change_devices") + ], + [ + InlineKeyboardButton(text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"), callback_data="subscription_manage_devices") + ] + ]) + if settings.is_traffic_selectable(): - keyboard.append([ - InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic") - ]) - keyboard.append([ + keyboard.insert(-2, [ InlineKeyboardButton(text=texts.t("SWITCH_TRAFFIC_BUTTON", "🔄 Переключить трафик"), callback_data="subscription_switch_traffic") ]) - - if settings.is_devices_selection_enabled(): - keyboard.append([ - InlineKeyboardButton( - text=texts.t("CHANGE_DEVICES_BUTTON", "📱 Изменить устройства"), - callback_data="subscription_change_devices" - ) + keyboard.insert(-2, [ + InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic") ]) - keyboard.append([ - InlineKeyboardButton( - text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"), - callback_data="subscription_manage_devices" - ) - ]) - + keyboard.append([ InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription") ]) - + return InlineKeyboardMarkup(inline_keyboard=keyboard) diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 99132589..b4d0666e 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -856,9 +856,6 @@ "DEVICE_CHANGE_NO_REFUND": "Payments are not refunded", "DEVICE_CHANGE_NO_REFUND_INFO": "ℹ️ Payments are not refunded", "DEVICE_CHANGE_RESULT_LINE": "📱 Was: {old} → Now: {new}\n", - "DEVICES_INVALID_REQUEST": "❌ Invalid request", - "DEVICES_INVALID_COUNT": "❌ Invalid device count", - "DEVICES_SELECTION_DISABLED": "⚠️ Device selection is unavailable", "DEVICE_CONNECTION_HELP": "❓ How to reconnect a device?", "DEVICE_FETCH_ERROR": "❌ Failed to load devices", "DEVICE_FETCH_INFO_ERROR": "❌ Failed to load device information", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 91c8812c..aa75bdd6 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -856,9 +856,6 @@ "DEVICE_CHANGE_NO_REFUND": "Возврат средств не производится", "DEVICE_CHANGE_NO_REFUND_INFO": "ℹ️ Возврат средств не производится", "DEVICE_CHANGE_RESULT_LINE": "📱 Было: {old} → Стало: {new}\n", - "DEVICES_INVALID_REQUEST": "❌ Некорректный запрос", - "DEVICES_INVALID_COUNT": "❌ Некорректное количество устройств", - "DEVICES_SELECTION_DISABLED": "⚠️ Выбор количества устройств недоступен", "DEVICE_CONNECTION_HELP": "❓ Как подключить устройство заново?", "DEVICE_FETCH_ERROR": "❌ Ошибка получения устройств", "DEVICE_FETCH_INFO_ERROR": "❌ Ошибка получения информации об устройствах", diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index a0125f70..857e4ea4 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -38,7 +38,6 @@ from app.database.crud.user import ( subtract_user_balance, cleanup_expired_promo_offer_discounts, ) -from app.utils.subscription_utils import resolve_hwid_device_limit from app.database.models import MonitoringLog, SubscriptionStatus, Subscription, User, Ticket, TicketStatus from app.localization.texts import get_texts from app.services.notification_settings_service import NotificationSettingsService @@ -284,26 +283,20 @@ class MonitoringService: logger.info(f"📝 Статус подписки {subscription.id} обновлен на 'expired'") async with self.api as api: - hwid_limit = resolve_hwid_device_limit(subscription) - - update_kwargs = dict( + updated_user = await api.update_user( uuid=user.remnawave_uuid, status=UserStatus.ACTIVE if is_active else UserStatus.EXPIRED, expire_at=subscription.end_date, traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb), traffic_limit_strategy=TrafficLimitStrategy.MONTH, + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=user.full_name, username=user.username, telegram_id=user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - update_kwargs['hwid_device_limit'] = hwid_limit - - updated_user = await api.update_user(**update_kwargs) subscription.subscription_url = updated_user.subscription_url subscription.subscription_crypto_link = updated_user.happ_crypto_link diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index c57f86fa..c7d6a279 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -38,7 +38,6 @@ from app.database.models import ( SubscriptionStatus, ServerSquad, ) -from app.utils.subscription_utils import resolve_hwid_device_limit logger = logging.getLogger(__name__) @@ -1217,53 +1216,44 @@ class RemnaWaveService: for user in users: if not user.subscription: continue - + try: subscription = user.subscription - hwid_limit = resolve_hwid_device_limit(subscription) - + if user.remnawave_uuid: - update_kwargs = dict( + await api.update_user( uuid=user.remnawave_uuid, status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED, expire_at=subscription.end_date, traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0, traffic_limit_strategy=TrafficLimitStrategy.MONTH, + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=user.full_name, username=user.username, telegram_id=user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - update_kwargs['hwid_device_limit'] = hwid_limit - - await api.update_user(**update_kwargs) stats["updated"] += 1 else: username = f"user_{user.telegram_id}" - - create_kwargs = dict( + + new_user = await api.create_user( username=username, expire_at=subscription.end_date, status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED, traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0, traffic_limit_strategy=TrafficLimitStrategy.MONTH, telegram_id=user.telegram_id, + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=user.full_name, username=user.username, telegram_id=user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - create_kwargs['hwid_device_limit'] = hwid_limit - - new_user = await api.create_user(**create_kwargs) await update_user(db, user, remnawave_uuid=new_user.uuid) subscription.remnawave_short_uuid = new_user.short_uuid diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index dfa00380..74f0877a 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -17,7 +17,6 @@ from app.utils.pricing_utils import ( calculate_prorated_price, validate_pricing_calculation ) -from app.utils.subscription_utils import resolve_hwid_device_limit logger = logging.getLogger(__name__) @@ -173,7 +172,6 @@ class SubscriptionService: return None async with self.get_api_client() as api: - hwid_limit = resolve_hwid_device_limit(subscription) existing_users = await api.get_user_by_telegram_id(user.telegram_id) if existing_users: logger.info(f"🔄 Найден существующий пользователь в панели для {user.telegram_id}") @@ -185,24 +183,20 @@ class SubscriptionService: except Exception as hwid_error: logger.warning(f"⚠️ Не удалось сбросить HWID: {hwid_error}") - update_kwargs = dict( + updated_user = await api.update_user( uuid=remnawave_user.uuid, status=UserStatus.ACTIVE, expire_at=subscription.end_date, traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb), traffic_limit_strategy=get_traffic_reset_strategy(), + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=user.full_name, username=user.username, telegram_id=user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - update_kwargs['hwid_device_limit'] = hwid_limit - - updated_user = await api.update_user(**update_kwargs) if reset_traffic: await self._reset_user_traffic( @@ -215,26 +209,22 @@ class SubscriptionService: else: logger.info(f"🆕 Создаем нового пользователя в панели для {user.telegram_id}") username = f"user_{user.telegram_id}" - create_kwargs = dict( + updated_user = await api.create_user( username=username, expire_at=subscription.end_date, status=UserStatus.ACTIVE, traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb), traffic_limit_strategy=get_traffic_reset_strategy(), telegram_id=user.telegram_id, + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=user.full_name, username=user.username, telegram_id=user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - if hwid_limit is not None: - create_kwargs['hwid_device_limit'] = hwid_limit - - updated_user = await api.create_user(**create_kwargs) - if reset_traffic: await self._reset_user_traffic( api, @@ -292,26 +282,20 @@ class SubscriptionService: logger.info(f"🔔 Статус подписки {subscription.id} автоматически изменен на 'expired'") async with self.get_api_client() as api: - hwid_limit = resolve_hwid_device_limit(subscription) - - update_kwargs = dict( + updated_user = await api.update_user( uuid=user.remnawave_uuid, status=UserStatus.ACTIVE if is_actually_active else UserStatus.EXPIRED, expire_at=subscription.end_date, traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb), traffic_limit_strategy=get_traffic_reset_strategy(), + hwid_device_limit=subscription.device_limit, description=settings.format_remnawave_user_description( full_name=user.full_name, username=user.username, telegram_id=user.telegram_id ), - active_internal_squads=subscription.connected_squads, + active_internal_squads=subscription.connected_squads ) - - if hwid_limit is not None: - update_kwargs['hwid_device_limit'] = hwid_limit - - updated_user = await api.update_user(**update_kwargs) if reset_traffic: await self._reset_user_traffic( diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index ce4f5709..ef0921bc 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -195,7 +195,6 @@ class BotConfigurationService: "DEFAULT_TRAFFIC_LIMIT_GB": "SUBSCRIPTIONS_CORE", "MAX_DEVICES_LIMIT": "SUBSCRIPTIONS_CORE", "PRICE_PER_DEVICE": "SUBSCRIPTIONS_CORE", - "DEVICES_SELECTION_ENABLED": "SUBSCRIPTIONS_CORE", "BASE_SUBSCRIPTION_PRICE": "SUBSCRIPTIONS_CORE", "DEFAULT_TRAFFIC_RESET_STRATEGY": "TRAFFIC", "RESET_TRAFFIC_ON_PAYMENT": "TRAFFIC", @@ -451,12 +450,6 @@ class BotConfigurationService: "example": "d4aa2b8c-9a36-4f31-93a2-6f07dad05fba", "warning": "Убедитесь, что выбранный сквад активен и доступен для подписки.", }, - "DEVICES_SELECTION_ENABLED": { - "description": "Разрешает пользователям выбирать количество устройств при покупке и продлении подписки.", - "format": "Булево значение.", - "example": "false", - "warning": "При отключении пользователи не смогут докупать устройства из интерфейса бота.", - }, "CRYPTOBOT_ENABLED": { "description": "Разрешает принимать криптоплатежи через CryptoBot.", "format": "Булево значение.", diff --git a/app/utils/subscription_utils.py b/app/utils/subscription_utils.py index d1ca5868..17d5dc6f 100644 --- a/app/utils/subscription_utils.py +++ b/app/utils/subscription_utils.py @@ -174,19 +174,3 @@ def convert_subscription_link_to_happ_scheme(subscription_link: Optional[str]) - return subscription_link return urlunparse(parsed_link._replace(scheme="happ")) - - -def resolve_hwid_device_limit(subscription: Optional[Subscription]) -> Optional[int]: - """Return a device limit value for RemnaWave payloads when selection is enabled.""" - - if subscription is None: - return None - - if not settings.is_devices_selection_enabled(): - return None - - limit = getattr(subscription, "device_limit", None) - if limit is None or limit <= 0: - return None - - return limit diff --git a/tests/test_subscription_cart_integration.py b/tests/test_subscription_cart_integration.py index 4f633e4b..eb65f2bf 100644 --- a/tests/test_subscription_cart_integration.py +++ b/tests/test_subscription_cart_integration.py @@ -142,80 +142,6 @@ async def test_return_to_saved_cart_success(mock_callback_query, mock_state, moc # В успешном сценарии вызывается callback.answer() mock_callback_query.answer.assert_called_once() - -@pytest.mark.asyncio -async def test_return_to_saved_cart_normalizes_devices_when_disabled( - mock_callback_query, - mock_state, - mock_user, - mock_db, -): - cart_data = { - 'period_days': 30, - 'countries': ['ru', 'us'], - 'devices': 5, - 'traffic_gb': 20, - 'total_price': 45000, - 'total_devices_price': 15000, - 'saved_cart': True, - 'user_id': mock_user.id, - } - - sanitized_summary_data = { - 'period_days': 30, - 'countries': ['ru', 'us'], - 'devices': 3, - 'traffic_gb': 20, - 'total_price': 30000, - 'total_devices_price': 0, - } - - with patch('app.handlers.subscription.purchase.user_cart_service') as mock_cart_service, \ - patch('app.handlers.subscription.purchase._get_available_countries') as mock_get_countries, \ - patch('app.handlers.subscription.purchase.format_period_description') as mock_format_period, \ - patch('app.localization.texts.get_texts') as mock_get_texts, \ - patch('app.handlers.subscription.purchase.get_subscription_confirm_keyboard_with_cart') as mock_keyboard_func, \ - patch('app.handlers.subscription.purchase.settings') as mock_settings, \ - patch('app.handlers.subscription.pricing._prepare_subscription_summary', new=AsyncMock(return_value=("ignored", sanitized_summary_data))): - - mock_cart_service.get_user_cart = AsyncMock(return_value=cart_data) - mock_cart_service.save_user_cart = AsyncMock() - mock_get_countries.return_value = [{'uuid': 'ru', 'name': 'Russia'}, {'uuid': 'us', 'name': 'USA'}] - mock_format_period.return_value = "30 дней" - mock_keyboard = AsyncMock() - mock_keyboard_func.return_value = mock_keyboard - - mock_texts = AsyncMock() - mock_texts.format_price = lambda x: f"{x/100} ₽" - mock_texts.t = lambda key, default=None: default or "" - mock_get_texts.return_value = mock_texts - - mock_settings.is_devices_selection_enabled.return_value = False - mock_settings.DEFAULT_DEVICE_LIMIT = 3 - mock_settings.is_traffic_fixed.return_value = False - mock_settings.get_fixed_traffic_limit.return_value = 0 - - mock_user.balance_kopeks = 60000 - - await return_to_saved_cart(mock_callback_query, mock_state, mock_user, mock_db) - - mock_cart_service.save_user_cart.assert_called_once() - _, saved_payload = mock_cart_service.save_user_cart.call_args[0] - assert saved_payload['devices'] == 3 - assert saved_payload['total_price'] == 30000 - assert saved_payload['saved_cart'] is True - - mock_state.set_data.assert_called_once() - normalized_data = mock_state.set_data.call_args[0][0] - assert normalized_data['devices'] == 3 - assert normalized_data['total_price'] == 30000 - assert normalized_data['saved_cart'] is True - - edited_text = mock_callback_query.message.edit_text.call_args[0][0] - assert "📱" not in edited_text - - mock_callback_query.answer.assert_called_once() - @pytest.mark.asyncio async def test_return_to_saved_cart_insufficient_funds(mock_callback_query, mock_state, mock_user, mock_db): """Тест возврата к сохраненной корзине с недостаточным балансом"""