diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index 3475588..fd927a5 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -30,7 +30,12 @@ class SettingField: i18n_description_key: Optional[str] = None -def _payment_presentation_fields(method_key: str, subsection: str) -> List[SettingField]: +def _payment_presentation_fields( + method_key: str, + subsection: str, + *, + default_icon: str, +) -> List[SettingField]: prefix = f"PAYMENT_{method_key}" return [ SettingField( @@ -56,6 +61,7 @@ def _payment_presentation_fields(method_key: str, subsection: str) -> List[Setti "WebApp button icon", "Lucide icon name rendered inside the Web App payment method button.", subsection=subsection, + placeholder=default_icon, ), SettingField( f"{prefix}_TELEGRAM_LABEL_RU", @@ -191,7 +197,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ # ─── Payment providers (toggles) ─────────────────────────────── # Common SettingField("STARS_ENABLED", "bool", "payments", "Telegram Stars", subsection="Общие"), - *_payment_presentation_fields("STARS", "Telegram Stars"), + *_payment_presentation_fields("STARS", "Telegram Stars", default_icon="Sparkles"), SettingField( "PAYMENT_METHODS_ORDER", "string", @@ -243,7 +249,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Принудительная привязка карты", subsection="YooKassa", ), - *_payment_presentation_fields("YOOKASSA", "YooKassa"), + *_payment_presentation_fields("YOOKASSA", "YooKassa", default_icon="CreditCard"), # FreeKassa SettingField("FREEKASSA_ENABLED", "bool", "payments", "Включена", subsection="FreeKassa"), SettingField( @@ -300,7 +306,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Через запятую — IP-адреса, с которых принимаются нотификации", subsection="FreeKassa", ), - *_payment_presentation_fields("FREEKASSA", "FreeKassa"), + *_payment_presentation_fields("FREEKASSA", "FreeKassa", default_icon="Smartphone"), # Platega SettingField("PLATEGA_ENABLED", "bool", "payments", "Включена", subsection="Platega"), SettingField( @@ -328,8 +334,8 @@ SETTINGS_MANIFEST: List[SettingField] = [ ), SettingField("PLATEGA_RETURN_URL", "url", "payments", "Return URL", subsection="Platega"), SettingField("PLATEGA_FAILED_URL", "url", "payments", "Failed URL", subsection="Platega"), - *_payment_presentation_fields("PLATEGA_SBP", "Platega SBP"), - *_payment_presentation_fields("PLATEGA_CRYPTO", "Platega Crypto"), + *_payment_presentation_fields("PLATEGA_SBP", "Platega SBP", default_icon="CreditCard"), + *_payment_presentation_fields("PLATEGA_CRYPTO", "Platega Crypto", default_icon="Bitcoin"), # SeverPay SettingField("SEVERPAY_ENABLED", "bool", "payments", "Включена", subsection="SeverPay"), SettingField("SEVERPAY_MID", "int", "payments", "MID", subsection="SeverPay"), @@ -355,7 +361,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ min=30, max=4320, ), - *_payment_presentation_fields("SEVERPAY", "SeverPay"), + *_payment_presentation_fields("SEVERPAY", "SeverPay", default_icon="CreditCard"), # Wata SettingField("WATA_ENABLED", "bool", "payments", "Enabled", subsection="Wata"), SettingField( @@ -410,7 +416,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Comma-separated IP addresses accepted for Wata webhooks.", subsection="Wata", ), - *_payment_presentation_fields("WATA", "Wata"), + *_payment_presentation_fields("WATA", "Wata", default_icon="WalletCards"), # CryptoPay SettingField("CRYPTOPAY_ENABLED", "bool", "payments", "Включена", subsection="CryptoPay"), SettingField( @@ -435,7 +441,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ SettingField( "CRYPTOPAY_ASSET", "string", "payments", "Asset", placeholder="RUB", subsection="CryptoPay" ), - *_payment_presentation_fields("CRYPTOPAY", "CryptoPay"), + *_payment_presentation_fields("CRYPTOPAY", "CryptoPay", default_icon="Bitcoin"), # ─── Trial ───────────────────────────────────────────────────── SettingField("TRIAL_ENABLED", "bool", "trial", "Триал включён"), SettingField("TRIAL_DURATION_DAYS", "int", "trial", "Длительность триала (дней)", min=0), diff --git a/backend/bot/handlers/user/subscription/core.py b/backend/bot/handlers/user/subscription/core.py index c50e9b1..13e0a21 100644 --- a/backend/bot/handlers/user/subscription/core.py +++ b/backend/bot/handlers/user/subscription/core.py @@ -11,6 +11,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy.ext.asyncio import AsyncSession from bot.keyboards.inline.user_keyboards import ( + callback_context_from_back_callback, + callback_suffix_for_context, get_autorenew_confirm_keyboard, get_back_to_main_menu_markup, get_hwid_device_packages_keyboard, @@ -19,6 +21,8 @@ from bot.keyboards.inline.user_keyboards import ( get_tariff_catalog_keyboard, get_tariff_packages_keyboard, get_tariff_periods_keyboard, + sale_mode_with_callback_context, + subscription_options_callback, ) from bot.middlewares.i18n import JsonI18n from bot.services.panel_api_service import PanelApiService @@ -56,11 +60,30 @@ def _has_multiple_enabled_tariffs(settings: Settings) -> bool: def _tariff_purchase_markup( - tariff, current_lang: str, i18n: JsonI18n, settings: Settings + tariff, + current_lang: str, + i18n: JsonI18n, + settings: Settings, + back_callback: str = "main_action:subscribe", + callback_context: Optional[str] = None, ) -> InlineKeyboardMarkup: if tariff.billing_model == "period": - return get_tariff_periods_keyboard(tariff, current_lang, i18n, settings) - return get_tariff_packages_keyboard(tariff, tariff.traffic_packages.rub, current_lang, i18n) + return get_tariff_periods_keyboard( + tariff, + current_lang, + i18n, + settings, + back_callback=back_callback, + callback_context=callback_context, + ) + return get_tariff_packages_keyboard( + tariff, + tariff.traffic_packages.rub, + current_lang, + i18n, + back_callback=back_callback, + callback_context=callback_context, + ) def _tariff_purchase_text(tariff, current_lang: str, i18n: JsonI18n, settings: Settings) -> str: @@ -98,13 +121,27 @@ async def display_subscription_options( tariffs_config = getattr(settings, "tariffs_config", None) if tariffs_config: enabled_tariffs = list(tariffs_config.enabled_tariffs) + callback_context = callback_context_from_back_callback(back_callback) if len(enabled_tariffs) == 1: tariff = enabled_tariffs[0] text_content = _tariff_purchase_text(tariff, current_lang, i18n, settings) - reply_markup = _tariff_purchase_markup(tariff, current_lang, i18n, settings) + reply_markup = _tariff_purchase_markup( + tariff, + current_lang, + i18n, + settings, + back_callback=back_callback, + callback_context=callback_context, + ) else: text_content = get_text("select_subscription_period") - reply_markup = get_tariff_catalog_keyboard(enabled_tariffs, current_lang, i18n) + reply_markup = get_tariff_catalog_keyboard( + enabled_tariffs, + current_lang, + i18n, + back_callback=back_callback, + callback_context=callback_context, + ) target_message_obj = event.message if isinstance(event, types.CallbackQuery) else event if isinstance(event, types.CallbackQuery): try: @@ -144,6 +181,7 @@ async def display_subscription_options( i18n, traffic_mode=traffic_mode, back_callback=back_callback, + callback_context=callback_context_from_back_callback(back_callback), ) else: text_content = get_text("no_subscription_options_available") @@ -193,13 +231,22 @@ async def select_tariff_callback( if not config or not callback.message: await callback.answer(get_text("error_occurred_try_again"), show_alert=True) return - tariff_key = callback.data.split(":", 2)[2] + parts = callback.data.split(":") + tariff_key = parts[2] if len(parts) > 2 else "" + callback_context = parts[3] if len(parts) > 3 else None try: tariff = config.require(tariff_key) except Exception: await callback.answer(get_text("error_try_again"), show_alert=True) return - markup = _tariff_purchase_markup(tariff, current_lang, i18n, settings) + markup = _tariff_purchase_markup( + tariff, + current_lang, + i18n, + settings, + back_callback=subscription_options_callback(callback_context), + callback_context=callback_context, + ) text = _tariff_purchase_text(tariff, current_lang, i18n, settings) await callback.message.edit_text(text, reply_markup=markup) await callback.answer() @@ -216,7 +263,12 @@ async def select_tariff_period_callback( if not config or not callback.message: await callback.answer(get_text("error_occurred_try_again"), show_alert=True) return - _, _, tariff_key, months_raw = callback.data.split(":", 3) + parts = callback.data.split(":") + if len(parts) < 4: + await callback.answer(get_text("error_try_again"), show_alert=True) + return + tariff_key, months_raw = parts[2], parts[3] + callback_context = parts[4] if len(parts) > 4 else None tariff = config.require(tariff_key) months = int(months_raw) price_rub = tariff.period_price(months, "rub") @@ -232,7 +284,10 @@ async def select_tariff_period_callback( current_lang, i18n, settings, - sale_mode=f"subscription@{tariff.key}", + sale_mode=sale_mode_with_callback_context( + f"subscription@{tariff.key}", callback_context + ), + back_callback=f"tariff:select:{tariff.key}{callback_suffix_for_context(callback_context)}", ) await callback.message.edit_text(get_text("choose_payment_method"), reply_markup=markup) await callback.answer() @@ -249,7 +304,12 @@ async def select_tariff_package_callback( if not config or not callback.message: await callback.answer(get_text("error_occurred_try_again"), show_alert=True) return - _, _, tariff_key, gb_raw = callback.data.split(":", 3) + parts = callback.data.split(":") + if len(parts) < 4: + await callback.answer(get_text("error_try_again"), show_alert=True) + return + tariff_key, gb_raw = parts[2], parts[3] + callback_context = parts[4] if len(parts) > 4 else None tariff = config.require(tariff_key) gb = float(gb_raw) packages = ( @@ -264,6 +324,12 @@ async def select_tariff_package_callback( sale_mode = ( f"{'traffic_package' if tariff.billing_model == 'traffic' else 'topup'}@{tariff.key}" ) + sale_mode = sale_mode_with_callback_context(sale_mode, callback_context) + back_callback = ( + f"tariff:select:{tariff.key}{callback_suffix_for_context(callback_context)}" + if tariff.billing_model == "traffic" + else "tariff_topup:list" + ) markup = get_payment_method_keyboard( gb, package.price, @@ -273,6 +339,7 @@ async def select_tariff_package_callback( i18n, settings, sale_mode=sale_mode, + back_callback=back_callback, ) await callback.message.edit_text(get_text("choose_payment_method_traffic"), reply_markup=markup) await callback.answer() @@ -384,6 +451,7 @@ async def select_tariff_premium_package_callback( i18n, settings, sale_mode=f"premium_topup@{tariff.key}", + back_callback="tariff_topup:list", ) await callback.message.edit_text(get_text("choose_payment_method_traffic"), reply_markup=markup) await callback.answer() @@ -462,6 +530,7 @@ async def hwid_devices_package_callback( i18n, settings, sale_mode=f"hwid_devices@{tariff.key}", + back_callback="hwid_devices:list", ) await callback.message.edit_text( get_text("choose_payment_method_hwid_devices"), reply_markup=markup @@ -731,6 +800,7 @@ async def tariff_change_pay_callback( i18n, settings, sale_mode=f"tariff_upgrade@{tariff_key}", + back_callback=f"tariff_change:confirm_pay:{tariff_key}:{amount_raw}", ) await callback.message.edit_text("Выберите способ оплаты", reply_markup=markup) await callback.answer() diff --git a/backend/bot/handlers/user/subscription/payments_subscription.py b/backend/bot/handlers/user/subscription/payments_subscription.py index 1e331a7..d988f63 100644 --- a/backend/bot/handlers/user/subscription/payments_subscription.py +++ b/backend/bot/handlers/user/subscription/payments_subscription.py @@ -4,7 +4,11 @@ from typing import Optional from aiogram import F, Router, types from sqlalchemy.ext.asyncio import AsyncSession -from bot.keyboards.inline.user_keyboards import get_payment_method_keyboard +from bot.keyboards.inline.user_keyboards import ( + get_payment_method_keyboard, + sale_mode_with_callback_context, + subscription_options_callback, +) from bot.middlewares.i18n import JsonI18n from config.settings import Settings @@ -32,8 +36,10 @@ async def select_subscription_period_callback_handler( traffic_packages = getattr(settings, "traffic_packages", {}) or {} stars_traffic_packages = getattr(settings, "stars_traffic_packages", {}) or {} traffic_mode = bool(getattr(settings, "traffic_sale_mode", False) or stars_traffic_packages) + parts = callback.data.split(":") + callback_context = parts[2] if len(parts) > 2 else None try: - months = float(callback.data.split(":")[-1]) + months = float(parts[1]) except (ValueError, IndexError): logging.error(f"Invalid subscription period in callback_data: {callback.data}") try: @@ -97,7 +103,10 @@ async def select_subscription_period_callback_handler( current_lang, i18n, settings, - sale_mode="traffic" if traffic_mode else "subscription", + sale_mode=sale_mode_with_callback_context( + "traffic" if traffic_mode else "subscription", callback_context + ), + back_callback=subscription_options_callback(callback_context), ) try: diff --git a/backend/bot/keyboards/inline/user_keyboards.py b/backend/bot/keyboards/inline/user_keyboards.py index 234d2a9..fa1ef76 100644 --- a/backend/bot/keyboards/inline/user_keyboards.py +++ b/backend/bot/keyboards/inline/user_keyboards.py @@ -5,6 +5,81 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton from config.settings import Settings +BOT_MENU_CONTEXT = "bot" + + +def callback_context_from_back_callback(back_callback: Optional[str]) -> Optional[str]: + if back_callback == "main_action:bot_interface": + return BOT_MENU_CONTEXT + return None + + +def sale_mode_with_callback_context(sale_mode: str, context: Optional[str]) -> str: + sale_mode = sale_mode or "subscription" + if not context or "|" in sale_mode: + return sale_mode + return f"{sale_mode}|{context}" + + +def callback_context_from_sale_mode(sale_mode: Optional[str]) -> Optional[str]: + if not sale_mode or "|" not in sale_mode: + return None + context = str(sale_mode).split("|", 1)[1].strip() + return context or None + + +def callback_suffix_for_context(context: Optional[str]) -> str: + return f":{context}" if context else "" + + +def subscription_options_callback(context: Optional[str]) -> str: + return "main_action:bot_subscribe" if context == BOT_MENU_CONTEXT else "main_action:subscribe" + + +def payment_methods_back_callback( + value: str, sale_mode: str = "subscription", price: Optional[float] = None +) -> str: + sale_mode = sale_mode or "subscription" + context = callback_context_from_sale_mode(sale_mode) + context_suffix = callback_suffix_for_context(context) + sale_mode_main = sale_mode.split("|", 1)[0] + sale_base = sale_mode_main.split("@", 1)[0] + tariff_key = sale_mode_main.split("@", 1)[1] if "@" in sale_mode_main else None + + if sale_base == "subscription" and tariff_key: + return f"tariff:period:{tariff_key}:{value}{context_suffix}" + if sale_base == "traffic_package" and tariff_key: + return f"tariff:package:{tariff_key}:{value}{context_suffix}" + if sale_base == "topup" and tariff_key: + return f"tariff:package:{tariff_key}:{value}" + if sale_base == "premium_topup" and tariff_key: + return f"tariff:premium_package:{tariff_key}:{value}" + if sale_base in {"hwid_device", "hwid_devices"} and tariff_key: + return f"hwid_devices:package:{tariff_key}:{value}" + if sale_base == "tariff_upgrade" and tariff_key: + amount = str(price) if price is not None else value + return f"tariff_change:pay:{tariff_key}:{amount}" + if sale_base in {"subscription", "traffic"}: + return f"subscribe_period:{value}{context_suffix}" + return subscription_options_callback(context) + + +def payment_options_back_callback(sale_mode: str = "subscription") -> str: + sale_mode = sale_mode or "subscription" + context = callback_context_from_sale_mode(sale_mode) + context_suffix = callback_suffix_for_context(context) + sale_mode_main = sale_mode.split("|", 1)[0] + sale_base = sale_mode_main.split("@", 1)[0] + tariff_key = sale_mode_main.split("@", 1)[1] if "@" in sale_mode_main else None + + if sale_base in {"subscription", "traffic_package"} and tariff_key: + return f"tariff:select:{tariff_key}{context_suffix}" + if sale_base in {"topup", "premium_topup"}: + return "tariff_topup:list" + if sale_base in {"hwid_device", "hwid_devices"}: + return "hwid_devices:list" + return subscription_options_callback(context) + def get_main_menu_inline_keyboard( lang: str, i18n_instance, settings: Settings, show_trial_button: bool = False @@ -187,9 +262,11 @@ def get_subscription_options_keyboard( i18n_instance, traffic_mode: bool = False, back_callback: str = "main_action:back_to_main", + callback_context: Optional[str] = None, ) -> InlineKeyboardMarkup: _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) builder = InlineKeyboardBuilder() + callback_context = callback_context or callback_context_from_back_callback(back_callback) def _format_gb(val: float) -> str: return str(int(val)) if float(val).is_integer() else f"{val:g}" @@ -204,7 +281,10 @@ def get_subscription_options_keyboard( price=price, currency_symbol=currency_symbol_val, ) - callback_data = f"subscribe_period:{_format_gb(months)}" + callback_data = ( + f"subscribe_period:{_format_gb(months)}" + f"{callback_suffix_for_context(callback_context)}" + ) else: button_text = _( "subscribe_for_months_button", @@ -212,7 +292,10 @@ def get_subscription_options_keyboard( price=price, currency_symbol=currency_symbol_val, ) - callback_data = f"subscribe_period:{months}" + callback_data = ( + f"subscribe_period:{months}" + f"{callback_suffix_for_context(callback_context)}" + ) builder.button(text=button_text, callback_data=callback_data) builder.adjust(1) builder.row( @@ -222,9 +305,14 @@ def get_subscription_options_keyboard( def get_tariff_catalog_keyboard( - tariffs: List[Any], lang: str, i18n_instance + tariffs: List[Any], + lang: str, + i18n_instance, + back_callback: str = "main_action:back_to_main", + callback_context: Optional[str] = None, ) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() + callback_context = callback_context or callback_context_from_back_callback(back_callback) for tariff in tariffs: label = tariff.name(lang) if tariff.billing_model == "period": @@ -235,20 +323,32 @@ def get_tariff_catalog_keyboard( package = tariff.min_traffic_package_rub() if package: label = f"{label} от {package.price:g} / {package.gb:g} GB" - builder.row(InlineKeyboardButton(text=label, callback_data=f"tariff:select:{tariff.key}")) + builder.row( + InlineKeyboardButton( + text=label, + callback_data=f"tariff:select:{tariff.key}" + f"{callback_suffix_for_context(callback_context)}", + ) + ) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) builder.row( InlineKeyboardButton( - text=_(key="back_to_main_menu_button"), callback_data="main_action:back_to_main" + text=_(key="back_to_main_menu_button"), callback_data=back_callback ) ) return builder.as_markup() def get_tariff_periods_keyboard( - tariff: Any, lang: str, i18n_instance, settings: Settings + tariff: Any, + lang: str, + i18n_instance, + settings: Settings, + back_callback: str = "main_action:subscribe", + callback_context: Optional[str] = None, ) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() + callback_context = callback_context or callback_context_from_back_callback(back_callback) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) for months in tariff.enabled_periods: rub_price = tariff.period_price(months, "rub") @@ -261,12 +361,13 @@ def get_tariff_periods_keyboard( price=rub_price, currency_symbol=settings.DEFAULT_CURRENCY_SYMBOL, ), - callback_data=f"tariff:period:{tariff.key}:{months}", + callback_data=f"tariff:period:{tariff.key}:{months}" + f"{callback_suffix_for_context(callback_context)}", ) ) builder.row( InlineKeyboardButton( - text=_(key="back_to_main_menu_button"), callback_data="main_action:subscribe" + text=_(key="back_to_main_menu_button"), callback_data=back_callback ) ) return builder.as_markup() @@ -278,8 +379,10 @@ def get_tariff_packages_keyboard( lang: str, i18n_instance, back_callback: str = "main_action:subscribe", + callback_context: Optional[str] = None, ) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() + callback_context = callback_context or callback_context_from_back_callback(back_callback) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) for package in packages: builder.row( @@ -290,7 +393,8 @@ def get_tariff_packages_keyboard( price=package.price, currency_symbol="RUB", ), - callback_data=f"tariff:package:{tariff.key}:{package.gb:g}", + callback_data=f"tariff:package:{tariff.key}:{package.gb:g}" + f"{callback_suffix_for_context(callback_context)}", ) ) builder.row( @@ -336,6 +440,7 @@ def get_payment_method_keyboard( i18n_instance, settings: Settings, sale_mode: str = "subscription", + back_callback: Optional[str] = None, ) -> InlineKeyboardMarkup: _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) builder = InlineKeyboardBuilder() @@ -371,7 +476,10 @@ def get_payment_method_keyboard( text=provider_telegram_button_text(spec, settings, _, language=lang), callback_data=callback_data, ) - builder.button(text=_(key="cancel_button"), callback_data="main_action:subscribe") + builder.button( + text=_(key="cancel_button"), + callback_data=back_callback or payment_options_back_callback(sale_mode), + ) builder.adjust(1) return builder.as_markup() @@ -403,6 +511,7 @@ def get_yk_autopay_choice_keyboard( i18n_instance, has_saved_cards: bool = True, sale_mode: str = "subscription", + back_callback: Optional[str] = None, ) -> InlineKeyboardMarkup: """Keyboard for choosing between saved card charge or new card payment when auto-renew is enabled.""" # noqa: E501 _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) @@ -430,7 +539,7 @@ def get_yk_autopay_choice_keyboard( builder.row( InlineKeyboardButton( text=_(key="back_to_payment_methods_button"), - callback_data=f"subscribe_period:{value_str}", + callback_data=back_callback or payment_methods_back_callback(value_str, sale_mode), ) ) return builder.as_markup() diff --git a/backend/bot/payment_providers/shared/callbacks.py b/backend/bot/payment_providers/shared/callbacks.py index c65ad31..67a7c8c 100644 --- a/backend/bot/payment_providers/shared/callbacks.py +++ b/backend/bot/payment_providers/shared/callbacks.py @@ -7,7 +7,10 @@ from typing import Optional from aiogram import types from sqlalchemy.ext.asyncio import AsyncSession -from bot.keyboards.inline.user_keyboards import get_payment_url_keyboard +from bot.keyboards.inline.user_keyboards import ( + get_payment_url_keyboard, + payment_methods_back_callback, +) from bot.middlewares.i18n import JsonI18n from db.dal import payment_dal from db.models import Payment @@ -155,7 +158,9 @@ async def render_payment_link( payment_url, current_lang, i18n, - back_callback=f"subscribe_period:{parts.human_value}", + back_callback=payment_methods_back_callback( + parts.human_value, parts.sale_mode, parts.price + ), back_text_key=back_text_key, ) await edit_or_answer( diff --git a/backend/bot/payment_providers/shared/common.py b/backend/bot/payment_providers/shared/common.py index be32e7a..5adb93c 100644 --- a/backend/bot/payment_providers/shared/common.py +++ b/backend/bot/payment_providers/shared/common.py @@ -123,7 +123,9 @@ def sale_mode_is_hwid_devices(sale_mode: str) -> bool: def sale_mode_tariff_key(sale_mode: str) -> Optional[str]: - return str(sale_mode or "").split("@", 1)[1] if "@" in str(sale_mode or "") else None + if "@" not in str(sale_mode or ""): + return None + return str(sale_mode).split("@", 1)[1].split("|", 1)[0] or None def format_number_for_payload(value: Any) -> str: diff --git a/backend/bot/payment_providers/stars.py b/backend/bot/payment_providers/stars.py index 3886551..699e2a9 100644 --- a/backend/bot/payment_providers/stars.py +++ b/backend/bot/payment_providers/stars.py @@ -6,6 +6,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, LabeledPri from aiohttp import web from sqlalchemy.ext.asyncio import AsyncSession +from bot.keyboards.inline.user_keyboards import payment_methods_back_callback from bot.middlewares.i18n import JsonI18n from bot.services.referral_service import ReferralService from bot.services.subscription_service import SubscriptionService @@ -210,7 +211,9 @@ async def pay_stars_callback_handler( [ InlineKeyboardButton( text=translator("back_to_payment_methods_button"), - callback_data=f"subscribe_period:{parts.human_value}", + callback_data=payment_methods_back_callback( + parts.human_value, parts.sale_mode, parts.price + ), ) ] ] diff --git a/backend/bot/payment_providers/yookassa.py b/backend/bot/payment_providers/yookassa.py index ceb7116..2a12b41 100644 --- a/backend/bot/payment_providers/yookassa.py +++ b/backend/bot/payment_providers/yookassa.py @@ -26,6 +26,7 @@ from bot.keyboards.inline.user_keyboards import ( get_payment_url_keyboard, get_yk_autopay_choice_keyboard, get_yk_saved_cards_keyboard, + payment_methods_back_callback, ) from bot.middlewares.i18n import JsonI18n from bot.services.lknpd_service import LknpdService @@ -1132,7 +1133,9 @@ async def _initiate_yk_payment( "description": payment_description, "subscription_duration_months": int(months) if sale_base == "subscription" else None, "sale_mode": sale_base, - "tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None, + "tariff_key": sale_mode.split("@", 1)[1].split("|", 1)[0] + if "@" in sale_mode + else None, "purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None, @@ -1458,6 +1461,9 @@ async def pay_yk_callback_handler( i18n, has_saved_cards=True, sale_mode=sale_mode, + back_callback=payment_methods_back_callback( + _format_value(months), sale_mode, price_rub + ), ), ) except Exception as e_edit: @@ -1472,6 +1478,9 @@ async def pay_yk_callback_handler( i18n, has_saved_cards=True, sale_mode=sale_mode, + back_callback=payment_methods_back_callback( + _format_value(months), sale_mode, price_rub + ), ), ) except Exception: @@ -1495,7 +1504,7 @@ async def pay_yk_callback_handler( price_rub=price_rub, currency_code_for_yk=currency_code_for_yk, save_payment_method=autopay_enabled and autopay_require_binding, - back_callback=f"subscribe_period:{_format_value(months)}", + back_callback=payment_methods_back_callback(_format_value(months), sale_mode, price_rub), sale_mode=sale_mode, ) try: @@ -1579,7 +1588,7 @@ async def pay_yk_new_card_handler( price_rub=price_rub, currency_code_for_yk=currency_code_for_yk, save_payment_method=autopay_enabled and autopay_require_binding, - back_callback=f"subscribe_period:{_format_value(months)}", + back_callback=payment_methods_back_callback(_format_value(months), sale_mode, price_rub), sale_mode=sale_mode, ) try: @@ -1671,6 +1680,9 @@ async def pay_yk_saved_list_handler( i18n, has_saved_cards=False, sale_mode=sale_mode, + back_callback=payment_methods_back_callback( + _format_value(months), sale_mode, price_rub + ), ), ) except Exception as e_edit: @@ -1685,6 +1697,9 @@ async def pay_yk_saved_list_handler( i18n, has_saved_cards=False, sale_mode=sale_mode, + back_callback=payment_methods_back_callback( + _format_value(months), sale_mode, price_rub + ), ), ) except Exception: diff --git a/backend/bot/services/subscription_service_impl/tariffs.py b/backend/bot/services/subscription_service_impl/tariffs.py index bf985e5..7ed111b 100644 --- a/backend/bot/services/subscription_service_impl/tariffs.py +++ b/backend/bot/services/subscription_service_impl/tariffs.py @@ -22,7 +22,10 @@ class TariffMixin: if separator in mode: base, suffix = mode.split(separator, 1) mode = base or mode - tariff_key = tariff_key or suffix or None + suffix_key = suffix.split("|", 1)[0] + if separator == "|" and suffix_key in {"bot"}: + suffix_key = "" + tariff_key = tariff_key or suffix_key or None break return mode, tariff_key diff --git a/frontend/src/admin/sections/SettingsSection.svelte b/frontend/src/admin/sections/SettingsSection.svelte index 10cf3fa..2a9e8fd 100644 --- a/frontend/src/admin/sections/SettingsSection.svelte +++ b/frontend/src/admin/sections/SettingsSection.svelte @@ -90,6 +90,23 @@ return key ? UiIcons[key] || null : null; } + function iconValue(field) { + return String(valueFor(field) || field?.placeholder || "").trim(); + } + + function iconIsDefault(field) { + return !String(valueFor(field) || "").trim(); + } + + function iconLabel(field) { + const iconName = iconValue(field); + if (!iconName) return at("settings_icon_empty", {}, "Default icon"); + if (iconIsDefault(field)) { + return at("settings_icon_default_value", { icon: iconName }, `Default: ${iconName}`); + } + return iconName; + } + function openIconPicker(field) { iconPickerField = field; iconPickerSearch = ""; @@ -211,7 +228,7 @@ oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)} /> {:else if field.type === "icon"} - {@const selectedIconName = valueFor(field) || ""} + {@const selectedIconName = iconValue(field)} {@const SelectedIcon = iconComponent(selectedIconName)} {/if} - {selectedIconName || at("settings_icon_empty", {}, "Default icon")} + {iconLabel(field)} - {#if selectedIconName} + {#if !iconIsDefault(field)}
- + {#if iconPickerField} + {@const currentIconName = iconValue(iconPickerField)} + {@const CurrentIcon = iconComponent(currentIconName)} +
+ + + {at("settings_icon_current", {}, "Current icon")} + {iconLabel(iconPickerField)} + + {#if !iconIsDefault(iconPickerField)} + settingsStore.markDirty(iconPickerField.key, "")} + > + + {at("settings_icon_use_default", {}, "Use default")} + + {/if} +
+ {/if} +
+ +
{#each filteredIconOptions as iconName} {@const Icon = iconComponent(iconName)}