feat: update tariff UI and mini app

This commit is contained in:
3252a8
2026-04-28 14:34:12 +03:00
parent 9deb98bbe7
commit 829758dc44
4 changed files with 326 additions and 10 deletions
+36
View File
@@ -2205,6 +2205,14 @@ def _serialize_subscription(
"traffic_used": _format_bytes(active.get("traffic_used_bytes")),
"traffic_limit_bytes": _coerce_int_or_none(active.get("traffic_limit_bytes")),
"traffic_used_bytes": _coerce_int_or_none(active.get("traffic_used_bytes")),
"tariff_key": active.get("tariff_key"),
"tariff_name": active.get("tariff_name"),
"tariff_description": active.get("tariff_description"),
"billing_model": active.get("billing_model"),
"tier_baseline_bytes": _coerce_int_or_none(active.get("tier_baseline_bytes")),
"topup_balance_bytes": _coerce_int_or_none(active.get("topup_balance_bytes")),
"period_start_at": active.get("period_start_at").isoformat() if active.get("period_start_at") else None,
"is_throttled": bool(active.get("is_throttled")),
"auto_renew_enabled": bool(getattr(local_sub, "auto_renew_enabled", False)),
"provider": getattr(local_sub, "provider", None),
}
@@ -2217,6 +2225,34 @@ def _serialize_plans(
subscription_options: Optional[Dict[int, float]] = None,
stars_subscription_options: Optional[Dict[int, int]] = None,
) -> List[Dict[str, Any]]:
if settings.tariffs_config:
plans: List[Dict[str, Any]] = []
for tariff in settings.tariffs_config.enabled_tariffs:
item: Dict[str, Any] = {
"tariff_key": tariff.key,
"billing_model": tariff.billing_model,
"title": tariff.name(lang),
"description": tariff.description(lang),
"squad_uuids": tariff.squad_uuids,
}
if tariff.billing_model == "period":
item["periods"] = [
{
"months": int(months),
"price": tariff.period_price(int(months), "rub"),
"stars_price": tariff.period_price(int(months), "stars"),
}
for months in tariff.enabled_periods
]
item["monthly_gb"] = tariff.monthly_gb
else:
item["traffic_packages"] = [
{"gb": package.gb, "price": package.price}
for package in (tariff.traffic_packages.rub if tariff.traffic_packages else [])
]
plans.append(item)
return plans
active_subscription_options = subscription_options or settings.subscription_options
active_stars_subscription_options = stars_subscription_options or settings.stars_subscription_options
plans: List[Dict[str, Any]] = []
+16 -9
View File
@@ -67,20 +67,23 @@ def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str, settings: S
'platega': 'Platega',
}.get(payment.provider, payment.provider or 'Unknown')
traffic_mode = getattr(settings, "traffic_sale_mode", False)
if traffic_mode:
traffic_val = payment.subscription_duration_months or 0
sale_base = (payment.sale_mode or "").split("@", 1)[0].split("|", 1)[0]
traffic_like = sale_base in {"traffic", "traffic_package", "topup"}
if traffic_like:
traffic_val = payment.purchased_gb or payment.subscription_duration_months or 0
traffic_display = str(int(traffic_val)) if float(traffic_val).is_integer() else f"{traffic_val:g}"
period_line = _("admin_payment_traffic_label", traffic_gb=traffic_display)
else:
period_line = _("admin_payment_months_label", months=payment.subscription_duration_months or 0)
tariff_line = f"\nTariff: {payment.tariff_key}" if payment.tariff_key else ""
sale_line = f"\nSale mode: {payment.sale_mode}" if payment.sale_mode else ""
return (
f"{status_emoji} <b>{payment.amount} {payment.currency}</b>\n"
f"👤 {user_info}\n"
f"💳 {provider_text}\n"
f"📅 {payment_date}\n"
f"{period_line}\n"
f"{period_line}{tariff_line}{sale_line}\n"
f"📋 {payment.status}\n"
f"📝 {payment.description or 'N/A'}"
)
@@ -212,20 +215,21 @@ async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data:
_("admin_csv_status"),
_("admin_csv_description"),
_("admin_csv_units"),
"sale_mode",
"tariff_key",
"purchased_gb",
_("admin_csv_created_at"),
_("admin_csv_provider_payment_id")
])
traffic_mode = getattr(settings, "traffic_sale_mode", False)
# Write payment data
for payment in all_payments:
units_val = payment.subscription_duration_months or ""
if traffic_mode and units_val not in ("", None):
units_val = payment.purchased_gb or payment.subscription_duration_months or ""
if (payment.purchased_gb is not None) and units_val not in ("", None):
try:
units_val = str(int(units_val)) if float(units_val).is_integer() else f"{units_val:g}"
except Exception:
units_val = payment.subscription_duration_months or ""
units_val = payment.purchased_gb or payment.subscription_duration_months or ""
writer.writerow([
payment.payment_id,
payment.user_id,
@@ -237,6 +241,9 @@ async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data:
payment.status,
payment.description or "",
units_val,
payment.sale_mode or "",
payment.tariff_key or "",
payment.purchased_gb or "",
payment.created_at.strftime('%Y-%m-%d %H:%M:%S') if payment.created_at else "",
payment.provider_payment_id or ""
])
+225
View File
@@ -13,6 +13,10 @@ from bot.keyboards.inline.user_keyboards import (
get_subscription_options_keyboard,
get_back_to_main_menu_markup,
get_autorenew_confirm_keyboard,
get_tariff_catalog_keyboard,
get_tariff_periods_keyboard,
get_tariff_packages_keyboard,
get_payment_method_keyboard,
)
from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService
@@ -57,6 +61,21 @@ async def display_subscription_options(event: Union[types.Message, types.Callbac
return
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
tariffs_config = getattr(settings, "tariffs_config", None)
if tariffs_config:
text_content = get_text("select_subscription_period")
reply_markup = get_tariff_catalog_keyboard(tariffs_config.enabled_tariffs, current_lang, i18n)
target_message_obj = event.message if isinstance(event, types.CallbackQuery) else event
if isinstance(event, types.CallbackQuery):
try:
await target_message_obj.edit_text(text_content, reply_markup=reply_markup)
except Exception:
await target_message_obj.answer(text_content, reply_markup=reply_markup)
await event.answer()
else:
await target_message_obj.answer(text_content, reply_markup=reply_markup)
return
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)
@@ -108,6 +127,199 @@ async def reshow_subscription_options_callback(callback: types.CallbackQuery, i1
await display_subscription_options(callback, i18n_data, settings, session)
@router.callback_query(F.data.startswith("tariff:select:"))
async def select_tariff_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
config = settings.tariffs_config
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]
try:
tariff = config.require(tariff_key)
except Exception:
await callback.answer(get_text("error_try_again"), show_alert=True)
return
if tariff.billing_model == "period":
markup = get_tariff_periods_keyboard(tariff, current_lang, i18n, settings)
else:
markup = get_tariff_packages_keyboard(tariff, tariff.traffic_packages.rub, current_lang, i18n)
text = f"{tariff.name(current_lang)}\n{tariff.description(current_lang)}".strip()
await callback.message.edit_text(text, reply_markup=markup)
await callback.answer()
@router.callback_query(F.data.startswith("tariff:period:"))
async def select_tariff_period_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
config = settings.tariffs_config
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)
tariff = config.require(tariff_key)
months = int(months_raw)
price_rub = tariff.period_price(months, "rub")
stars_price = tariff.period_price(months, "stars")
if price_rub is None:
await callback.answer(get_text("error_try_again"), show_alert=True)
return
markup = get_payment_method_keyboard(
months,
price_rub,
int(stars_price) if stars_price else None,
settings.DEFAULT_CURRENCY_SYMBOL,
current_lang,
i18n,
settings,
sale_mode=f"subscription@{tariff.key}",
)
await callback.message.edit_text(get_text("choose_payment_method"), reply_markup=markup)
await callback.answer()
@router.callback_query(F.data.startswith("tariff:package:"))
async def select_tariff_package_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
config = settings.tariffs_config
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)
tariff = config.require(tariff_key)
gb = float(gb_raw)
packages = tariff.traffic_packages.rub if tariff.billing_model == "traffic" else (config.topup_packages_for(tariff).rub if config.topup_packages_for(tariff) else [])
package = next((pkg for pkg in packages if float(pkg.gb) == gb), None)
if not package:
await callback.answer(get_text("error_try_again"), show_alert=True)
return
sale_mode = f"{'traffic_package' if tariff.billing_model == 'traffic' else 'topup'}@{tariff.key}"
markup = get_payment_method_keyboard(
gb,
package.price,
None,
settings.DEFAULT_CURRENCY_SYMBOL,
current_lang,
i18n,
settings,
sale_mode=sale_mode,
)
await callback.message.edit_text(get_text("choose_payment_method_traffic"), reply_markup=markup)
await callback.answer()
@router.callback_query(F.data == "tariff_topup:list")
async def tariff_topup_list_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
config = settings.tariffs_config
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
if not config or not active or not active.get("tariff_key") or not callback.message:
await callback.answer(get_text("error_try_again"), show_alert=True)
return
tariff = config.require(active["tariff_key"])
packages = config.topup_packages_for(tariff)
rub_packages = packages.rub if packages else []
if not rub_packages:
await callback.answer(get_text("no_subscription_options_available"), show_alert=True)
return
markup = get_tariff_packages_keyboard(tariff, rub_packages, current_lang, i18n, back_callback="main_action:my_subscription")
await callback.message.edit_text(get_text("choose_payment_method_traffic"), reply_markup=markup)
await callback.answer()
@router.callback_query(F.data == "tariff_change:list")
async def tariff_change_list_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
config = settings.tariffs_config
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
if not config or not active or not callback.message:
await callback.answer("Error", show_alert=True)
return
rows = []
for tariff in config.enabled_tariffs:
if tariff.key == active.get("tariff_key"):
continue
rows.append([InlineKeyboardButton(text=tariff.name(current_lang), callback_data=f"tariff_change:select:{tariff.key}")])
rows.append([InlineKeyboardButton(text=i18n.gettext(current_lang, "back_to_main_menu_button"), callback_data="main_action:my_subscription")])
await callback.message.edit_text("Выберите тариф", reply_markup=InlineKeyboardMarkup(inline_keyboard=rows))
await callback.answer()
@router.callback_query(F.data.startswith("tariff_change:select:"))
async def tariff_change_select_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
config = settings.tariffs_config
if not config or not callback.message:
await callback.answer("Error", show_alert=True)
return
tariff_key = callback.data.split(":", 2)[2]
target = config.require(tariff_key)
db_sub = await subscription_dal.get_active_subscription_by_user_id(session, callback.from_user.id)
if not db_sub:
await callback.answer("Error", show_alert=True)
return
options = subscription_service.calculate_tariff_switch_options(db_sub, target)
rows = []
if options["mode"] == "period_to_period":
rows.append([InlineKeyboardButton(text=f"Без доплаты, дней станет {options['recalc_days']}", callback_data=f"tariff_change:apply:{target.key}:recalc_days")])
if options.get("paid_diff_rub", 0) > 0:
rows.append([InlineKeyboardButton(text=f"Доплатить {options['paid_diff_rub']} RUB", callback_data=f"tariff_change:pay:{target.key}:{options['paid_diff_rub']}")])
elif options["mode"] == "period_to_traffic":
rows.append([InlineKeyboardButton(text=f"Перейти без доплаты, получить {options['converted_gb']} GB", callback_data=f"tariff_change:apply:{target.key}:convert_days_to_gb")])
for package in target.traffic_packages.rub:
rows.append([InlineKeyboardButton(text=f"+ {package.gb:g} GB за {package.price:g} RUB", callback_data=f"tariff:package:{target.key}:{package.gb:g}")])
else:
for months in target.enabled_periods:
price = target.period_price(months, "rub")
if price:
rows.append([InlineKeyboardButton(text=f"{months} мес. за {price:g} RUB", callback_data=f"tariff:period:{target.key}:{months}")])
rows.append([InlineKeyboardButton(text=i18n.gettext(current_lang, "back_to_main_menu_button"), callback_data="tariff_change:list")])
await callback.message.edit_text(f"{target.name(current_lang)}\n{target.description(current_lang)}".strip(), reply_markup=InlineKeyboardMarkup(inline_keyboard=rows))
await callback.answer()
@router.callback_query(F.data.startswith("tariff_change:apply:"))
async def tariff_change_apply_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, subscription_service: SubscriptionService, session: AsyncSession):
_, _, tariff_key, mode = callback.data.split(":", 3)
result = await subscription_service.switch_tariff_without_payment(session, callback.from_user.id, tariff_key, mode)
if result:
await session.commit()
await callback.answer("Готово", show_alert=True)
await my_subscription_command_handler(callback, i18n_data, settings, subscription_service.panel_service, subscription_service, session, callback.bot)
else:
await callback.answer("Error", show_alert=True)
@router.callback_query(F.data.startswith("tariff_change:pay:"))
async def tariff_change_pay_callback(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: JsonI18n = i18n_data.get("i18n_instance")
_, _, tariff_key, amount_raw = callback.data.split(":", 3)
amount = float(amount_raw)
markup = get_payment_method_keyboard(
1,
amount,
None,
settings.DEFAULT_CURRENCY_SYMBOL,
current_lang,
i18n,
settings,
sale_mode=f"tariff_upgrade@{tariff_key}",
)
await callback.message.edit_text("Выберите способ оплаты", reply_markup=markup)
await callback.answer()
async def my_subscription_command_handler(
event: Union[types.Message, types.CallbackQuery],
i18n_data: dict,
@@ -215,6 +427,11 @@ async def my_subscription_command_handler(
config_link=config_link_value,
)
else:
tariff_prefix = ""
if active.get("tariff_name"):
tariff_prefix = f"🎟 {active.get('tariff_name')}\n"
if active.get("tariff_description"):
tariff_prefix += f"{active.get('tariff_description')}\n"
text = get_text(
"my_subscription_details",
end_date=end_date.strftime("%Y-%m-%d") if end_date else "N/A",
@@ -230,6 +447,8 @@ async def my_subscription_command_handler(
),
traffic_period=period_label,
)
if tariff_prefix:
text = tariff_prefix + "\n" + text
base_markup = get_back_to_main_menu_markup(current_lang, i18n)
kb = base_markup.inline_keyboard
@@ -324,6 +543,12 @@ async def my_subscription_command_handler(
InlineKeyboardButton(text=get_text("payment_methods_manage_button"), callback_data="pm:manage")
])
if settings.tariffs_config and local_sub and local_sub.tariff_key:
prepend_rows.append([
InlineKeyboardButton(text="Сменить тариф", callback_data="tariff_change:list"),
InlineKeyboardButton(text="Докупить трафик", callback_data="tariff_topup:list"),
])
if prepend_rows:
kb = prepend_rows + kb
except Exception:
+49 -1
View File
@@ -1,6 +1,6 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
from aiogram.types import InlineKeyboardMarkup, WebAppInfo
from typing import Dict, Optional, List, Tuple
from typing import Dict, Optional, List, Tuple, Any
from config.settings import Settings
@@ -185,6 +185,54 @@ def get_subscription_options_keyboard(subscription_options: Dict[
return builder.as_markup()
def get_tariff_catalog_keyboard(tariffs: List[Any], lang: str, i18n_instance) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for tariff in tariffs:
label = tariff.name(lang)
if tariff.billing_model == "period":
min_price = tariff.min_period_price_rub()
if min_price is not None:
label = f"{label} от {min_price:g}"
else:
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}"))
_ = 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"))
return builder.as_markup()
def get_tariff_periods_keyboard(tariff: Any, lang: str, i18n_instance, settings: Settings) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
for months in tariff.enabled_periods:
rub_price = tariff.period_price(months, "rub")
if rub_price and rub_price > 0:
builder.row(
InlineKeyboardButton(
text=_("subscribe_for_months_button", months=months, price=rub_price, currency_symbol=settings.DEFAULT_CURRENCY_SYMBOL),
callback_data=f"tariff:period:{tariff.key}:{months}",
)
)
builder.row(InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data="main_action:subscribe"))
return builder.as_markup()
def get_tariff_packages_keyboard(tariff: Any, packages: List[Any], lang: str, i18n_instance, back_callback: str = "main_action:subscribe") -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
for package in packages:
builder.row(
InlineKeyboardButton(
text=_("buy_traffic_package_button", traffic_gb=f"{package.gb:g}", price=package.price, currency_symbol="RUB"),
callback_data=f"tariff:package:{tariff.key}:{package.gb:g}",
)
)
builder.row(InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data=back_callback))
return builder.as_markup()
def get_payment_method_keyboard(months: int, price: float,
stars_price: Optional[int],
currency_symbol_val: str, lang: str,