fix: fix menus and modal views after payment providers refactor
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)}
|
||||
<AdminButton
|
||||
class="admin-icon-picker-trigger"
|
||||
@@ -221,9 +238,9 @@
|
||||
{#if SelectedIcon}
|
||||
<svelte:component this={SelectedIcon} size={16} />
|
||||
{/if}
|
||||
<span>{selectedIconName || at("settings_icon_empty", {}, "Default icon")}</span>
|
||||
<span>{iconLabel(field)}</span>
|
||||
</AdminButton>
|
||||
{#if selectedIconName}
|
||||
{#if !iconIsDefault(field)}
|
||||
<AdminButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -424,20 +441,47 @@
|
||||
class="admin-icon-picker-dialog"
|
||||
>
|
||||
<div class="admin-icon-picker-body">
|
||||
<label class="admin-icon-picker-search">
|
||||
<Search size={15} />
|
||||
<input
|
||||
bind:value={iconPickerSearch}
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder={at("search", {}, "Search")}
|
||||
/>
|
||||
</label>
|
||||
{#if iconPickerField}
|
||||
{@const currentIconName = iconValue(iconPickerField)}
|
||||
{@const CurrentIcon = iconComponent(currentIconName)}
|
||||
<div class="admin-icon-picker-current">
|
||||
<span class="admin-icon-picker-current-preview" aria-hidden="true">
|
||||
{#if CurrentIcon}
|
||||
<svelte:component this={CurrentIcon} size={24} />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="admin-icon-picker-current-meta">
|
||||
<small>{at("settings_icon_current", {}, "Current icon")}</small>
|
||||
<strong>{iconLabel(iconPickerField)}</strong>
|
||||
</span>
|
||||
{#if !iconIsDefault(iconPickerField)}
|
||||
<AdminButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onclick={() => settingsStore.markDirty(iconPickerField.key, "")}
|
||||
>
|
||||
<X size={12} />
|
||||
{at("settings_icon_use_default", {}, "Use default")}
|
||||
</AdminButton>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="admin-icon-picker-toolbar">
|
||||
<label class="admin-icon-picker-search">
|
||||
<Search size={15} />
|
||||
<input
|
||||
bind:value={iconPickerSearch}
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder={at("search", {}, "Search")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-icon-picker-grid">
|
||||
{#each filteredIconOptions as iconName}
|
||||
{@const Icon = iconComponent(iconName)}
|
||||
<button
|
||||
class:active={iconPickerField && valueFor(iconPickerField) === iconName}
|
||||
class:active={iconPickerField && iconValue(iconPickerField) === iconName}
|
||||
class="admin-icon-picker-option"
|
||||
type="button"
|
||||
onclick={() => selectIcon(iconName)}
|
||||
|
||||
@@ -1892,12 +1892,64 @@
|
||||
}
|
||||
|
||||
.admin-icon-picker-dialog {
|
||||
width: min(100%, 720px);
|
||||
width: min(100%, 760px);
|
||||
}
|
||||
|
||||
.admin-icon-picker-body {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-current {
|
||||
display: grid;
|
||||
grid-template-columns: 52px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 10px;
|
||||
background:
|
||||
linear-gradient(135deg, var(--surface-sheen), transparent),
|
||||
color-mix(in srgb, var(--admin-surface-2) 86%, transparent);
|
||||
}
|
||||
|
||||
.admin-icon-picker-current-preview {
|
||||
display: grid;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
place-items: center;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--admin-border));
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--surface-muted));
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.admin-icon-picker-current-meta {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-current-meta small {
|
||||
color: var(--admin-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-icon-picker-current-meta strong {
|
||||
overflow: hidden;
|
||||
color: var(--admin-text);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-icon-picker-toolbar {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-search {
|
||||
@@ -1921,28 +1973,33 @@
|
||||
|
||||
.admin-icon-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: min(52vh, 460px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
padding: 2px 4px 4px 0;
|
||||
}
|
||||
|
||||
.admin-icon-picker-option {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
gap: 10px;
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--admin-text);
|
||||
padding: 9px 10px;
|
||||
padding: 11px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-icon-picker-option > svg {
|
||||
justify-self: center;
|
||||
color: var(--admin-muted);
|
||||
}
|
||||
|
||||
.admin-icon-picker-option span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1955,6 +2012,30 @@
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.admin-icon-picker-option.active > svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.admin-icon-picker-current {
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-icon-picker-current > .admin-btn {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.admin-icon-picker-current-preview {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.admin-setting-control .admin-color {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
@@ -273,6 +273,7 @@ def test_common_sale_mode_helpers_cover_provider_payment_records():
|
||||
assert sale_mode_is_traffic("premium_topup@vip")
|
||||
assert sale_mode_is_hwid_devices("hwid_devices@vip")
|
||||
assert sale_mode_tariff_key("subscription@vip") == "vip"
|
||||
assert sale_mode_tariff_key("subscription@vip|bot") == "vip"
|
||||
assert format_number_for_payload(10.0) == "10"
|
||||
assert format_number_for_payload(10.5) == "10.5"
|
||||
|
||||
|
||||
@@ -10,8 +10,13 @@ from bot.keyboards.inline.user_keyboards import (
|
||||
get_information_links_keyboard,
|
||||
get_language_selection_keyboard,
|
||||
get_main_menu_inline_keyboard,
|
||||
get_payment_method_keyboard,
|
||||
get_referral_link_keyboard,
|
||||
get_subscription_options_keyboard,
|
||||
get_tariff_catalog_keyboard,
|
||||
get_tariff_periods_keyboard,
|
||||
payment_methods_back_callback,
|
||||
payment_options_back_callback,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +99,73 @@ class UserBotMenuTests(unittest.TestCase):
|
||||
self.assertIn("main_action:bot_interface", self._callback_data(referral_markup))
|
||||
self.assertIn("main_action:bot_interface", self._callback_data(info_markup))
|
||||
self.assertIn("set_lang_ru:bot", self._callback_data(language_markup))
|
||||
self.assertIn("subscribe_period:1:bot", self._callback_data(subscription_markup))
|
||||
|
||||
def test_payment_navigation_context_keeps_bot_menu_source(self):
|
||||
settings = SimpleNamespace(
|
||||
payment_methods_order=[],
|
||||
PLATEGA_ENABLED=False,
|
||||
PLATEGA_SBP_ENABLED=False,
|
||||
PLATEGA_CRYPTO_ENABLED=False,
|
||||
)
|
||||
markup = get_payment_method_keyboard(
|
||||
1,
|
||||
100,
|
||||
None,
|
||||
"RUB",
|
||||
"en",
|
||||
self.i18n,
|
||||
settings,
|
||||
sale_mode="subscription|bot",
|
||||
)
|
||||
|
||||
self.assertIn("main_action:bot_subscribe", self._callback_data(markup))
|
||||
self.assertEqual(
|
||||
payment_methods_back_callback("1", "subscription|bot"),
|
||||
"subscribe_period:1:bot",
|
||||
)
|
||||
|
||||
def test_tariff_back_buttons_return_to_previous_level(self):
|
||||
tariff = SimpleNamespace(
|
||||
key="basic",
|
||||
billing_model="period",
|
||||
enabled_periods=[1],
|
||||
name=lambda _lang: "Basic",
|
||||
description=lambda _lang: "Basic plan",
|
||||
min_period_price_rub=lambda: 100,
|
||||
period_price=lambda months, currency: (
|
||||
100 if months == 1 and currency == "rub" else None
|
||||
),
|
||||
)
|
||||
settings = SimpleNamespace(DEFAULT_CURRENCY_SYMBOL="RUB")
|
||||
|
||||
catalog = get_tariff_catalog_keyboard(
|
||||
[tariff],
|
||||
"en",
|
||||
self.i18n,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
periods = get_tariff_periods_keyboard(
|
||||
tariff,
|
||||
"en",
|
||||
self.i18n,
|
||||
settings,
|
||||
back_callback="main_action:bot_subscribe",
|
||||
callback_context="bot",
|
||||
)
|
||||
|
||||
self.assertIn("tariff:select:basic:bot", self._callback_data(catalog))
|
||||
self.assertIn("main_action:bot_interface", self._callback_data(catalog))
|
||||
self.assertIn("tariff:period:basic:1:bot", self._callback_data(periods))
|
||||
self.assertIn("main_action:bot_subscribe", self._callback_data(periods))
|
||||
self.assertEqual(
|
||||
payment_options_back_callback("subscription@basic|bot"),
|
||||
"tariff:select:basic:bot",
|
||||
)
|
||||
self.assertEqual(
|
||||
payment_methods_back_callback("1", "subscription@basic|bot"),
|
||||
"tariff:period:basic:1:bot",
|
||||
)
|
||||
|
||||
def test_webapp_referral_link_uses_ref_query_and_is_normalized(self):
|
||||
link = referral._build_webapp_referral_link(
|
||||
|
||||
Reference in New Issue
Block a user