feat: premium squads inside one tariff

This commit is contained in:
3252a8
2026-05-10 12:51:53 +03:00
parent 6f9a87cf98
commit 990eba1e1e
34 changed files with 1898 additions and 135 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import logging
import logging
import csv
import io
from aiogram import Router, F, types
@@ -68,7 +68,7 @@ def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str, settings: S
}.get(payment.provider, payment.provider or 'Unknown')
sale_base = (payment.sale_mode or "").split("@", 1)[0].split("|", 1)[0]
traffic_like = sale_base in {"traffic", "traffic_package", "topup"}
traffic_like = sale_base in {"traffic", "traffic_package", "topup", "premium_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}"
+2 -2
View File
@@ -1,4 +1,4 @@
import logging
import logging
import json
import asyncio
from datetime import datetime, timezone, timedelta
@@ -238,7 +238,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
promo_code_id_from_payment=promo_code_id,
provider="yookassa",
sale_mode=sale_mode,
traffic_gb=traffic_amount_gb if sale_mode_base in {"traffic", "traffic_package", "topup"} else None,
traffic_gb=traffic_amount_gb if sale_mode_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
)
if not activation_details or not activation_details.get('end_date'):
+115 -4
View File
@@ -1,8 +1,10 @@
import hashlib
import html
import logging
from aiogram import Router, F, types, Bot
from aiogram.filters import Command
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder
from typing import Optional, Union
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
@@ -269,10 +271,80 @@ async def tariff_topup_list_callback(callback: types.CallbackQuery, i18n_data: d
tariff = config.require(active["tariff_key"])
packages = config.topup_packages_for(tariff)
rub_packages = packages.rub if packages else []
if not rub_packages:
premium_packages = tariff.premium_topup_packages.rub if tariff.premium_topup_packages else []
if not rub_packages and not premium_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")
builder = InlineKeyboardBuilder()
currency = settings.DEFAULT_CURRENCY_SYMBOL
for package in rub_packages:
builder.row(
InlineKeyboardButton(
text=f"Обычный трафик +{package.gb:g} GB — {package.price:g} {currency}",
callback_data=f"tariff:package:{tariff.key}:{package.gb:g}",
)
)
for package in premium_packages:
builder.row(
InlineKeyboardButton(
text=f"Premium-серверы +{package.gb:g} GB — {package.price:g} {currency}",
callback_data=f"tariff:premium_package:{tariff.key}:{package.gb:g}",
)
)
builder.row(InlineKeyboardButton(text=get_text("back_to_main_menu_button"), callback_data="main_action:my_subscription"))
premium_lines = []
carryover_lines = []
if rub_packages or premium_packages:
carryover_lines.append("Докупленный трафик не сгорает: сначала расходуется месячный лимит, затем докупленный остаток.")
if int(active.get("premium_limit_bytes") or 0) > 0:
premium_left = max(0, int(active.get("premium_limit_bytes") or 0) - int(active.get("premium_used_bytes") or 0))
labels = active.get("premium_node_labels") or active.get("premium_squad_labels") or []
if labels:
visible = [str(label) for label in labels[:8]]
premium_lines.append("Premium-лимит действует на:")
premium_lines.extend(f"{label}" for label in visible)
if len(labels) > len(visible):
premium_lines.append(f"• ... еще {len(labels) - len(visible)}")
premium_lines.append(
f"Premium использовано: {active.get('premium_used')} из {active.get('premium_limit')}. Осталось: {premium_left / 2**30:.2f} GB."
)
text = get_text("choose_payment_method_traffic")
if carryover_lines:
text = text + "\n\n" + "\n".join(carryover_lines)
if premium_lines:
text = text + "\n\n" + "\n".join(premium_lines)
await callback.message.edit_text(text, reply_markup=builder.as_markup())
await callback.answer()
@router.callback_query(F.data.startswith("tariff:premium_package:"))
async def select_tariff_premium_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.premium_topup_packages.rub if tariff.premium_topup_packages 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
markup = get_payment_method_keyboard(
gb,
package.price,
None,
settings.DEFAULT_CURRENCY_SYMBOL,
current_lang,
i18n,
settings,
sale_mode=f"premium_topup@{tariff.key}",
)
await callback.message.edit_text(get_text("choose_payment_method_traffic"), reply_markup=markup)
await callback.answer()
@@ -350,6 +422,9 @@ async def tariff_change_list_callback(callback: types.CallbackQuery, i18n_data:
if not config or not active or not callback.message:
await callback.answer("Error", show_alert=True)
return
if len(config.enabled_tariffs) <= 1:
await callback.answer("Смена тарифа недоступна: сейчас включен только один тариф.", show_alert=True)
return
rows = []
for tariff in config.enabled_tariffs:
if tariff.key == active.get("tariff_key"):
@@ -614,6 +689,31 @@ async def my_subscription_command_handler(
if tariff_prefix:
text = tariff_prefix + "\n" + text
if int(active.get("premium_limit_bytes") or 0) > 0:
premium_limit = int(active.get("premium_limit_bytes") or 0)
premium_used = int(active.get("premium_used_bytes") or 0)
premium_left = max(0, premium_limit - premium_used)
premium_balance = int(active.get("premium_topup_balance_bytes") or 0)
premium_status = "ограничен" if active.get("premium_is_limited") else "активен"
labels = active.get("premium_node_labels") or active.get("premium_squad_labels") or []
if labels:
visible = [html.escape(str(label)) for label in labels[:8]]
label_block = "\n".join(f"{label}" for label in visible)
if len(labels) > len(visible):
label_block += f"\n• ... еще {len(labels) - len(visible)}"
else:
label_block = "• premium-серверы тарифа"
text += (
"\n\n🚀 <b>Premium-серверы</b>\n"
f"Статус: <b>{premium_status}</b>\n"
f"Лимит: <b>{active.get('premium_used')} из {active.get('premium_limit')}</b>\n"
f"Осталось: <b>{premium_left / 2**30:.2f} GB</b>\n"
f"Докупленный остаток: <b>{premium_balance / 2**30:.2f} GB</b>\n"
"Отдельный лимит действует на:\n"
f"{label_block}\n\n"
"Premium-докупка не сгорает: сначала расходуется месячный лимит premium-серверов, затем докупленный premium-трафик."
)
base_markup = get_back_to_main_menu_markup(
current_lang,
i18n,
@@ -727,8 +827,19 @@ async def my_subscription_command_handler(
tariff_actions = []
if _has_multiple_enabled_tariffs(settings):
tariff_actions.append(InlineKeyboardButton(text="Сменить тариф", callback_data="tariff_change:list"))
tariff_actions.append(InlineKeyboardButton(text="Докупить трафик", callback_data="tariff_topup:list"))
prepend_rows.append(tariff_actions)
try:
tariff = settings.tariffs_config.require(local_sub.tariff_key)
topup_packages = settings.tariffs_config.topup_packages_for(tariff)
has_topup_packages = bool(
(topup_packages and topup_packages.has_any())
or (tariff.premium_topup_packages and tariff.premium_topup_packages.has_any())
)
except Exception:
has_topup_packages = False
if has_topup_packages:
tariff_actions.append(InlineKeyboardButton(text="Докупить трафик", callback_data="tariff_topup:list"))
if tariff_actions:
prepend_rows.append(tariff_actions)
if prepend_rows:
kb = prepend_rows + kb
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional
from aiogram import F, Router, types
from sqlalchemy.ext.asyncio import AsyncSession
@@ -30,7 +30,7 @@ async def pay_crypto_callback_handler(
pass
return
if not cryptopay_service or not getattr(cryptopay_service, "configured", False):
if not settings.CRYPTOPAY_ENABLED or not cryptopay_service or not getattr(cryptopay_service, "configured", False):
try:
await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True)
except Exception:
@@ -55,7 +55,7 @@ async def pay_crypto_callback_handler(
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
payment_description = (
get_text("payment_description_traffic", traffic_gb=human_value)
if sale_base in {"traffic", "traffic_package", "topup"}
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
)
@@ -72,7 +72,7 @@ async def pay_crypto_callback_handler(
try:
await callback.message.edit_text(
get_text(
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup"} else "payment_link_message",
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
months=int(months),
traffic_gb=human_value,
),
@@ -89,7 +89,7 @@ async def pay_crypto_callback_handler(
try:
await callback.message.answer(
get_text(
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup"} else "payment_link_message",
key="payment_link_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_link_message",
months=int(months),
traffic_gb=human_value,
),
@@ -1,4 +1,4 @@
import logging
import logging
from datetime import datetime
from typing import Optional
@@ -64,7 +64,7 @@ async def pay_fk_callback_handler(
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
payment_description = (
get_text("payment_description_traffic", traffic_gb=human_value)
if sale_base in {"traffic", "traffic_package", "topup"}
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
)
currency_code = getattr(freekassa_service, "default_currency", None) or settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
@@ -79,7 +79,7 @@ async def pay_fk_callback_handler(
"provider": "freekassa",
"sale_mode": sale_mode,
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
}
@@ -1,4 +1,4 @@
import json
import json
import logging
from typing import Optional
@@ -91,7 +91,7 @@ async def pay_platega_callback_handler(
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
payment_description = (
get_text("payment_description_traffic", traffic_gb=human_value)
if sale_base in {"traffic", "traffic_package", "topup"}
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
)
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
@@ -106,7 +106,7 @@ async def pay_platega_callback_handler(
"provider": "platega",
"sale_mode": sale_mode,
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
}
@@ -1,4 +1,4 @@
import logging
import logging
from typing import Optional
from aiogram import F, Router, types
@@ -63,7 +63,7 @@ async def pay_severpay_callback_handler(
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
payment_description = (
get_text("payment_description_traffic", traffic_gb=human_value)
if sale_base in {"traffic", "traffic_package", "topup"}
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
)
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
@@ -78,7 +78,7 @@ async def pay_severpay_callback_handler(
"provider": "severpay",
"sale_mode": sale_mode,
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
}
@@ -1,4 +1,4 @@
import logging
import logging
from typing import Optional
from aiogram import F, Router, types
@@ -56,7 +56,7 @@ async def pay_stars_callback_handler(
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
payment_description = (
get_text("payment_description_traffic", traffic_gb=human_value)
if sale_base in {"traffic", "traffic_package", "topup"}
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
)
@@ -73,7 +73,7 @@ async def pay_stars_callback_handler(
try:
await callback.message.edit_text(
get_text(
"payment_invoice_sent_message_traffic" if sale_base in {"traffic", "traffic_package", "topup"} else "payment_invoice_sent_message",
"payment_invoice_sent_message_traffic" if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else "payment_invoice_sent_message",
months=int(months),
traffic_gb=human_value,
),
@@ -1,4 +1,4 @@
import logging
import logging
from typing import List, Optional, Tuple
from aiogram import F, Router, types
@@ -84,7 +84,7 @@ async def _initiate_yk_payment(
sale_base = _sale_mode_base(sale_mode)
payment_description = (
get_text("payment_description_traffic", traffic_gb=_format_value(months))
if sale_base in {"traffic", "traffic_package", "topup"}
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
else (get_text("payment_description_hwid_devices", count=int(months)) if sale_base in {"hwid_device", "hwid_devices"} else get_text("payment_description_subscription", months=int(months)))
)
payment_record_data = {
@@ -96,7 +96,7 @@ async def _initiate_yk_payment(
"subscription_duration_months": int(months),
"sale_mode": sale_base,
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None,
"purchased_hwid_devices": int(months) if sale_base in {"hwid_device", "hwid_devices"} else None,
}
@@ -132,7 +132,7 @@ async def _initiate_yk_payment(
"payment_db_id": str(db_payment_record.payment_id),
"sale_mode": sale_mode,
}
if sale_base in {"traffic", "traffic_package", "topup"}:
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}:
yookassa_metadata["traffic_gb"] = str(months)
if payment_method_id:
yookassa_metadata["used_saved_payment_method_id"] = payment_method_id