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/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/purchase.py b/app/handlers/subscription/purchase.py index a858c034..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, @@ -1249,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() @@ -1339,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, 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 de24da46..2656392f 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -2085,33 +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") ]) - if settings.is_traffic_selectable(): - keyboard.append([ - InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic") - ]) - keyboard.append([ - 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.append([ - InlineKeyboardButton(text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"), callback_data="subscription_manage_devices") + 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.insert(-2, [ + InlineKeyboardButton(text=texts.t("SWITCH_TRAFFIC_BUTTON", "🔄 Переключить трафик"), callback_data="subscription_switch_traffic") + ]) + keyboard.insert(-2, [ + InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic") + ]) + 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/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": "Булево значение.",