Files
vpn-telegram-bot/handlers_shop.py
T

1724 lines
59 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Баланс, тарифы, оплата с баланса и криптой."""
import time
import uuid
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, LabeledPrice, Message, PreCheckoutQuery
from remnawave import RemnawaveSDK
from keyboards import (
device_confirm_inline,
device_packs_inline,
digiseller_cancel_inline,
digiseller_inline,
invoice_inline,
renew_confirm_inline,
renew_tariffs_inline,
stars_amounts_inline,
subscription_inline,
tariffs_inline,
topup_amounts_inline,
topup_code_inline,
topup_providers_inline,
traffic_confirm_inline,
traffic_packs_inline,
)
from config import Settings
from services.anypay import Anypay, AnypayError
from services.cryptopay import CryptoPay, CryptoPayError
from services.db import Database
from services.devices import (
BASE_DEVICES,
EXTRA_DEVICE_PRICE_RUB,
get_device_pack,
)
from services.digiseller import Digiseller, DigisellerError
from services.digiseller_fulfill import fulfill_digiseller_unique_code
from services.heleket import Heleket, HeleketError
from services.lava import Lava, LavaError
from services.payments import fulfill_paid_invoice
from services.purchase import (
get_my_subscription_info,
purchase_device_pack,
purchase_tariff,
purchase_traffic_pack,
)
from services.stars import create_stars_topup, parse_stars_payload
from services.tariffs import Tariff, get_tariff
from services.topup_code_redeem import TopupCodeError, redeem_topup_code
from services.traffic import (
BASE_TRAFFIC_GB,
PACK_GB,
PACK_PRICE_RUB,
format_gb,
get_traffic_pack,
)
from services.transactions_ui import transaction_label
router = Router(name="shop")
class DigisellerStates(StatesGroup):
waiting_code = State()
class TopupCodeStates(StatesGroup):
waiting_code = State()
def _any_crypto(cryptopay: CryptoPay, heleket: Heleket) -> bool:
return cryptopay.enabled or heleket.enabled
def _pay_kb(
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller | None = None,
lava: Lava | None = None,
tariff_id: str | None = None,
*,
anypay: Anypay | None = None,
settings: Settings | None = None,
stars_enabled: bool | None = None,
):
from keyboards import balance_inline, tariff_pay_methods_inline
digi_on = bool(digiseller and digiseller.enabled)
lava_on = bool(lava and lava.enabled)
anypay_on = bool(anypay and anypay.enabled)
if stars_enabled is None:
stars_on = bool(settings.stars_enabled) if settings else True
else:
stars_on = bool(stars_enabled)
if tariff_id:
return tariff_pay_methods_inline(
tariff_id,
crypto_enabled=cryptopay.enabled,
heleket_enabled=heleket.enabled,
lava_enabled=lava_on,
anypay_enabled=anypay_on,
)
return balance_inline(
crypto_enabled=cryptopay.enabled,
heleket_enabled=heleket.enabled,
digiseller_enabled=digi_on,
lava_enabled=lava_on,
anypay_enabled=anypay_on,
stars_enabled=stars_on,
)
def _tariff_card(t: Tariff, balance: float) -> str:
traffic = format_gb(t.traffic_gb if t.traffic_gb > 0 else BASE_TRAFFIC_GB, digits=0)
return (
f"<b>🛒 {t.title}</b>\n\n"
f"{t.description}\n\n"
f"⏱ Срок: <b>{t.days} дн.</b>\n"
f"📦 Трафик: <b>{traffic}</b> (база)\n"
f"📱 Устройств: <b>{BASE_DEVICES}</b> (база)\n"
f"💵 Цена: <b>{t.format_price()}</b>\n\n"
f"Твой баланс: <b>{balance:.0f} ₽</b>\n\n"
f"Докупка трафика: +{format_gb(PACK_GB, digits=0)} = {int(PACK_PRICE_RUB)}\n"
f"Доп. устройство: +1 = {int(EXTRA_DEVICE_PRICE_RUB)} ₽ "
f"(при превышении подписка отключается)\n"
"Оплата: с баланса или криптой (Crypto Bot / Heleket)."
)
def _safe_order_id(raw: str) -> str:
cleaned = "".join(ch if ch.isalnum() or ch in "_-" else "_" for ch in raw)
return cleaned[:128]
@router.message(F.text == "💰 Баланс")
async def cmd_balance(
message: Message,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
anypay: Anypay,
settings: Settings,
) -> None:
user = message.from_user
assert user
bot_user = db.ensure_user(
user.id,
username=user.username,
full_name=user.full_name,
)
txs = db.recent_transactions(user.id, limit=5)
lines = [
"<b>💰 Баланс</b>\n",
f"Доступно: <b>{bot_user.balance:.0f} ₽</b>\n",
"Пополнить: <b>Stars</b>, <b>картой/СБП</b> (Lava / AnyPay), <b>криптой</b> или <b>кодом</b>.",
]
if txs:
lines.append("\n<b>Последние операции:</b>")
for tx in txs:
sign = "+" if tx["amount"] > 0 else ""
label = transaction_label(str(tx["kind"]), tx.get("meta"))
lines.append(f"• {sign}{float(tx['amount']):.0f} ₽ · {label}")
await message.answer(
"\n".join(lines),
reply_markup=_pay_kb(
cryptopay, heleket, digiseller, lava, anypay=anypay, settings=settings
),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:balance")
async def cb_pay_balance(
callback: CallbackQuery,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
anypay: Anypay,
state: FSMContext,
settings: Settings,
) -> None:
await state.clear()
user = callback.from_user
if not user:
await callback.answer()
return
balance = db.get_balance(user.id)
await callback.answer()
if callback.message:
await callback.message.edit_text(
f"<b>💰 Баланс</b>\n\nДоступно: <b>{balance:.0f} ₽</b>",
reply_markup=_pay_kb(
cryptopay, heleket, digiseller, lava, anypay=anypay, settings=settings
),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:stars")
async def cb_pay_stars(callback: CallbackQuery, settings: Settings) -> None:
if not settings.stars_enabled:
await callback.answer("Stars выключены", show_alert=True)
return
await callback.answer()
if callback.message:
rate = settings.stars_rub_per_star
await callback.message.edit_text(
"<b>⭐ Пополнение через Telegram Stars</b>\n\n"
f"Курс: <b>1 ⭐ ≈ {rate:g} ₽</b> на баланс.\n"
"Выбери сумму — оплата прямо в Telegram, без внешних сайтов.",
reply_markup=stars_amounts_inline(rub_per_star=rate),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("pay:stars_amount:"))
async def cb_pay_stars_amount(
callback: CallbackQuery,
db: Database,
settings: Settings,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
) -> None:
if not settings.stars_enabled:
await callback.answer("Stars выключены", show_alert=True)
return
user = callback.from_user
if not user or not callback.message:
await callback.answer()
return
try:
amount = float((callback.data or "").split(":")[-1])
except ValueError:
await callback.answer("Неверная сумма", show_alert=True)
return
if amount <= 0:
await callback.answer("Неверная сумма", show_alert=True)
return
await callback.answer("Счёт Stars…")
try:
created = await create_stars_topup(
bot=callback.bot,
db=db,
telegram_id=user.id,
amount_rub=amount,
rub_per_star=settings.stars_rub_per_star,
)
# Чат (включая Telegram Web): инвойс + запасная ссылка
await callback.message.answer_invoice(
title="Пополнение баланса",
description=(
f"Зачисление {created['rub_label']} на баланс VPN Service "
f"({created['stars_label']})"
),
payload=created["payload"],
currency="XTR",
prices=[
LabeledPrice(
label=f"Баланс {created['rub_label']}",
amount=int(created["stars"]),
)
],
provider_token="",
)
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
await callback.message.answer(
f"Счёт на <b>{created['stars_label']}</b> → "
f"<b>{created['rub_label']}</b> на баланс.\n\n"
"Оплати инвойс выше или кнопку ниже "
"(удобно в <b>Telegram Web</b> / мобильном).",
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=f"⭐ Оплатить {created['stars_label']}",
url=created["invoice_link"],
)
],
[
InlineKeyboardButton(
text="💰 Баланс",
callback_data="pay:balance",
)
],
]
),
)
except Exception as exc: # noqa: BLE001
await callback.message.answer(
f"Не удалось создать счёт Stars.\n<code>{exc}</code>",
parse_mode="HTML",
reply_markup=_pay_kb(
cryptopay, heleket, digiseller, lava, settings=settings
),
)
@router.pre_checkout_query()
async def on_pre_checkout(query: PreCheckoutQuery) -> None:
parsed = parse_stars_payload(query.invoice_payload or "")
if not parsed:
await query.answer(ok=False, error_message="Неверный платёж")
return
await query.answer(ok=True)
@router.message(F.successful_payment)
async def on_successful_payment(
message: Message,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
settings: Settings,
) -> None:
payment = message.successful_payment
if not payment or payment.currency != "XTR":
return
user = message.from_user
if not user:
return
parsed = parse_stars_payload(payment.invoice_payload or "")
if not parsed:
await message.answer("Оплата получена, но payload не распознан. Напиши админу.")
return
invoice_id, telegram_id, amount_rub = parsed
if telegram_id != user.id:
await message.answer("Оплата не совпала с аккаунтом. Напиши админу.")
return
local = db.get_invoice(invoice_id, provider="stars")
if not local:
# fallback: credit anyway once, record meta by charge id
charge = payment.telegram_payment_charge_id
balance = db.add_balance(
user.id,
amount_rub,
kind="stars_topup",
meta=f"stars:orphan:{charge}",
)
await message.answer(
f"✅ <b>Баланс пополнен</b>\n\n"
f"+{amount_rub:.0f} ₽ через Telegram Stars\n"
f"Доступно: <b>{balance:.0f} ₽</b>",
parse_mode="HTML",
reply_markup=_pay_kb(
cryptopay, heleket, digiseller, lava, settings=settings
),
)
return
if local.get("status") == "paid":
await message.answer(
"Этот платёж уже зачислен.",
reply_markup=_pay_kb(
cryptopay, heleket, digiseller, lava, settings=settings
),
)
return
if not db.mark_invoice_paid(invoice_id, provider="stars"):
await message.answer(
"Этот платёж уже зачислен.",
reply_markup=_pay_kb(
cryptopay, heleket, digiseller, lava, settings=settings
),
)
return
amount = float(local["amount_rub"])
charge = payment.telegram_payment_charge_id
balance = db.add_balance(
user.id,
amount,
kind="stars_topup",
meta=f"stars:{invoice_id}:{charge}",
)
await message.answer(
f"✅ <b>Баланс пополнен</b>\n\n"
f"+{amount:.0f} ₽ через Telegram Stars ({payment.total_amount} ⭐)\n"
f"Доступно: <b>{balance:.0f} ₽</b>",
parse_mode="HTML",
reply_markup=_pay_kb(cryptopay, heleket, digiseller, lava, settings=settings),
)
@router.callback_query(F.data == "pay:digiseller")
async def cb_pay_digiseller(
callback: CallbackQuery,
digiseller: Digiseller,
state: FSMContext,
) -> None:
if not digiseller.enabled:
await callback.answer("Digiseller не настроен", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer()
return
await state.clear()
await callback.answer()
pay_url = digiseller.pay_url_for(user.id)
if callback.message:
await callback.message.edit_text(
"<b>💳 Пополнение через Digiseller</b>\n\n"
"1) Нажми «Оплатить на Digiseller»\n"
"2) Оплати товар на сайте\n"
"3) Digiseller <b>сам перенаправит</b> на проверку — "
"баланс зачислится <b>автоматически</b>\n"
"4) В бот придёт уведомление о пополнении\n\n"
"<i>Код вводить не нужно. Ручной ввод — только если автозачисление "
"не сработало.</i>",
reply_markup=digiseller_inline(pay_url=pay_url),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:digiseller_code")
async def cb_pay_digiseller_code(
callback: CallbackQuery,
digiseller: Digiseller,
state: FSMContext,
) -> None:
if not digiseller.enabled:
await callback.answer("Digiseller не настроен", show_alert=True)
return
await state.set_state(DigisellerStates.waiting_code)
await callback.answer()
if callback.message:
await callback.message.edit_text(
"<b>🔑 Ручной ввод кода Digiseller</b>\n\n"
"Нужен только если автозачисление не сработало.\n"
"Отправь уникальный код сообщением.",
reply_markup=digiseller_cancel_inline(),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:digiseller_cancel")
async def cb_pay_digiseller_cancel(
callback: CallbackQuery,
state: FSMContext,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
) -> None:
await state.clear()
user = callback.from_user
balance = db.get_balance(user.id) if user else 0
await callback.answer("Отменено")
if callback.message:
await callback.message.edit_text(
f"<b>💰 Баланс</b>\n\nДоступно: <b>{balance:.0f} ₽</b>",
reply_markup=_pay_kb(cryptopay, heleket, digiseller, lava),
parse_mode="HTML",
)
@router.message(DigisellerStates.waiting_code)
async def digiseller_got_code(
message: Message,
state: FSMContext,
db: Database,
digiseller: Digiseller,
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
) -> None:
user = message.from_user
if not user:
return
code = (message.text or "").strip()
if not code or code.startswith("/"):
await message.answer(
"Пришли уникальный код Digiseller или нажми «Отмена».",
reply_markup=digiseller_cancel_inline(),
)
return
wait = await message.answer("⏳ Проверяю уникальный код…")
try:
result = await fulfill_digiseller_unique_code(
db=db,
digiseller=digiseller,
unique_code=code,
telegram_id_hint=user.id,
bot=None,
notify=False,
)
await state.clear()
await wait.edit_text(
result.message,
reply_markup=_pay_kb(cryptopay, heleket, digiseller, lava),
parse_mode="HTML",
)
except DigisellerError as exc:
await wait.edit_text(
f"Не удалось проверить код.\n<code>{exc}</code>\n\n"
"Проверь код и попробуй ещё раз, либо нажми «Отмена».",
reply_markup=digiseller_cancel_inline(),
parse_mode="HTML",
)
except Exception as exc: # noqa: BLE001
await wait.edit_text(
f"Ошибка проверки.\n<code>{exc}</code>",
reply_markup=digiseller_cancel_inline(),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:topup_code")
async def cb_pay_topup_code(
callback: CallbackQuery,
state: FSMContext,
settings: Settings,
) -> None:
await state.set_state(TopupCodeStates.waiting_code)
await callback.answer()
pay_url = settings.topup_code_pay_url
lines = [
"<b>🎫 Пополнение по коду</b>\n",
"Для <b>карты и СБП</b> — покупай код (на странице оплаты "
"<b>более 40 способов</b>).\n",
"Напрямую в боте без кода — только <b>крипта</b>.\n",
"Отправь код сообщением (XXXX-XXXX-XXXX-XXXX). Один код — одно пополнение.",
]
if pay_url:
lines.append("\nКупить код — кнопка ниже.")
if callback.message:
await callback.message.edit_text(
"\n".join(lines),
reply_markup=topup_code_inline(pay_url=pay_url),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:topup_code_cancel")
async def cb_pay_topup_code_cancel(
callback: CallbackQuery,
state: FSMContext,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
) -> None:
await state.clear()
user = callback.from_user
balance = db.get_balance(user.id) if user else 0
await callback.answer("Отменено")
if callback.message:
await callback.message.edit_text(
f"<b>💰 Баланс</b>\n\nДоступно: <b>{balance:.0f} ₽</b>",
reply_markup=_pay_kb(cryptopay, heleket, digiseller, lava),
parse_mode="HTML",
)
@router.message(TopupCodeStates.waiting_code)
async def topup_code_got_code(
message: Message,
state: FSMContext,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
digiseller: Digiseller,
lava: Lava,
) -> None:
user = message.from_user
if not user:
return
code = (message.text or "").strip()
if not code or code.startswith("/"):
await message.answer(
"Пришли код пополнения или нажми «Отмена».",
reply_markup=topup_code_inline(),
)
return
wait = await message.answer("⏳ Проверяю код…")
try:
result = redeem_topup_code(db, telegram_id=user.id, raw_code=code)
await state.clear()
await wait.edit_text(
result.message,
reply_markup=_pay_kb(cryptopay, heleket, digiseller, lava),
parse_mode="HTML",
)
except TopupCodeError as exc:
await wait.edit_text(
f"Не удалось активировать код.\n<code>{exc}</code>",
reply_markup=topup_code_inline(),
parse_mode="HTML",
)
except Exception as exc: # noqa: BLE001
await wait.edit_text(
f"Ошибка.\n<code>{exc}</code>",
reply_markup=topup_code_inline(),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:lava")
async def cb_pay_lava(callback: CallbackQuery, lava: Lava) -> None:
if not lava.enabled:
await callback.answer("Lava не настроен", show_alert=True)
return
await callback.answer()
if callback.message:
await callback.message.edit_text(
"<b>💳 Пополнение через Lava</b>\n\n"
"Карта / СБП. Выбери сумму — после оплаты баланс зачислится автоматически.",
reply_markup=topup_amounts_inline("lava"),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:anypay")
async def cb_pay_anypay(callback: CallbackQuery, anypay: Anypay) -> None:
if not anypay.enabled:
await callback.answer("AnyPay не настроен", show_alert=True)
return
await callback.answer()
if callback.message:
await callback.message.edit_text(
"<b>💳 Пополнение через AnyPay</b>\n\n"
"Карта, СБП, крипта и др. Сумма от 10 до 3000 ₽. "
"После оплаты баланс зачислится автоматически.",
reply_markup=topup_amounts_inline("anypay"),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("shop:buy_lava:"))
async def cb_shop_buy_lava(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
lava: Lava,
) -> None:
if not lava.enabled:
await callback.answer("Lava не настроен", show_alert=True)
return
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer()
return
await callback.answer("Создаю счёт…")
order_id = _safe_order_id(f"tariff_{tariff.id}_{user.id}_{int(time.time())}")
try:
inv = await lava.create_invoice(
amount_rub=float(tariff.price),
order_id=order_id,
comment=f"Tariff {tariff.id} tg={user.id}",
custom_fields=str(user.id),
)
except LavaError as exc:
if callback.message:
await callback.message.edit_text(
f"Не удалось создать счёт Lava.\n<code>{exc}</code>",
parse_mode="HTML",
)
return
local_id = db.next_local_invoice_id()
db.create_invoice_record(
telegram_id=user.id,
invoice_id=local_id,
kind="tariff",
amount_rub=float(tariff.price),
pay_url=inv.pay_url,
payload=None,
provider="lava",
tariff_id=tariff.id,
order_id=order_id,
external_uuid=inv.invoice_id,
)
if callback.message:
await callback.message.edit_text(
f"<b>💳 Счёт Lava · {tariff.title}</b>\n\n"
f"Сумма: <b>{tariff.format_price()}</b>\n"
"Оплати картой или СБП — тариф активируется автоматически.",
reply_markup=invoice_inline(local_id, inv.pay_url, provider="lava"),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("shop:buy_anypay:"))
async def cb_shop_buy_anypay(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
anypay: Anypay,
) -> None:
if not anypay.enabled:
await callback.answer("AnyPay не настроен", show_alert=True)
return
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer()
return
if float(tariff.price) < 10 or float(tariff.price) > 3000:
await callback.answer(
"Для тарифа вне 10–3000 ₽ оплатите с баланса или другим способом",
show_alert=True,
)
return
await callback.answer("Создаю счёт…")
local_id = db.next_local_invoice_id()
pay_id = str(local_id)
try:
inv = await anypay.create_invoice(
pay_id=pay_id,
amount_rub=float(tariff.price),
desc=f"Tariff {tariff.id} tg={user.id}",
)
except AnypayError as exc:
if callback.message:
await callback.message.edit_text(
f"Не удалось создать счёт AnyPay.\n<code>{exc}</code>",
parse_mode="HTML",
)
return
db.create_invoice_record(
telegram_id=user.id,
invoice_id=local_id,
kind="tariff",
amount_rub=float(tariff.price),
pay_url=inv.pay_url,
payload=pay_id,
provider="anypay",
tariff_id=tariff.id,
order_id=pay_id,
external_uuid=inv.transaction_id,
)
if callback.message:
await callback.message.edit_text(
f"<b>💳 Счёт AnyPay · {tariff.title}</b>\n\n"
f"Сумма: <b>{tariff.format_price()}</b>\n"
"Оплати на странице AnyPay — тариф активируется автоматически.",
reply_markup=invoice_inline(local_id, inv.pay_url, provider="anypay"),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("pay:check_lava:"))
async def cb_pay_check_lava(
callback: CallbackQuery,
db: Database,
sdk: RemnawaveSDK,
tariffs: list[Tariff],
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
) -> None:
if not lava.enabled:
await callback.answer("Lava не настроен", show_alert=True)
return
invoice_id = int((callback.data or "").split(":")[-1])
local = db.get_invoice(invoice_id, provider="lava")
if not local:
await callback.answer("Счёт не найден", show_alert=True)
return
if local["telegram_id"] != (callback.from_user.id if callback.from_user else 0):
await callback.answer("Это не твой счёт", show_alert=True)
return
await callback.answer("Проверяю…")
order_id = str(local.get("order_id") or "")
external = str(local.get("external_uuid") or "")
try:
remote = await lava.get_invoice(
order_id=order_id or None,
invoice_id=external or None,
)
except LavaError as exc:
if callback.message:
await callback.message.answer(
f"Ошибка проверки: <code>{exc}</code>", parse_mode="HTML"
)
return
status = remote.status if remote else "unknown"
if remote is None or not lava.is_paid(status):
await callback.answer(f"Ещё не оплачено ({status})", show_alert=True)
return
result = await fulfill_paid_invoice(db=db, sdk=sdk, tariffs=tariffs, local=local)
if callback.message:
await callback.message.edit_text(
result.message,
reply_markup=_pay_kb(cryptopay, heleket, lava=lava),
parse_mode="HTML",
)
@router.callback_query(F.data == "pay:topup")
async def cb_pay_topup(
callback: CallbackQuery,
cryptopay: CryptoPay,
heleket: Heleket,
) -> None:
if not _any_crypto(cryptopay, heleket):
await callback.answer("Крипто-оплата не настроена", show_alert=True)
return
await callback.answer()
if not callback.message:
return
if cryptopay.enabled and not heleket.enabled:
await callback.message.edit_text(
"<b>🪙 Пополнение криптой</b>\n\n"
"Без кода — только криптовалюта. Карта и СБП — через "
"<b>код пополнения</b> (40+ способов на странице покупки).\n\n"
"Выбери сумму в рублях.\n"
"Оплата через @CryptoBot (USDT / TON / BTC…).",
reply_markup=topup_amounts_inline("cryptobot"),
parse_mode="HTML",
)
return
if heleket.enabled and not cryptopay.enabled:
await callback.message.edit_text(
"<b>🪙 Пополнение криптой</b>\n\n"
"Без кода — только криптовалюта. Карта и СБП — код пополнения.\n\n"
"Выбери сумму в рублях.\n"
"Оплата через Heleket.",
reply_markup=topup_amounts_inline("heleket"),
parse_mode="HTML",
)
return
await callback.message.edit_text(
"<b>🪙 Пополнение криптой</b>\n\n"
"Напрямую без кода — только крипта. Карта / СБП — «Код пополнения».\n\n"
"Выбери платёжку:\n"
"• <b>Crypto Bot</b> — основная\n"
"• <b>Heleket</b> — резервная",
reply_markup=topup_providers_inline(
crypto_enabled=cryptopay.enabled,
heleket_enabled=heleket.enabled,
),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("pay:topup_provider:"))
async def cb_pay_topup_provider(callback: CallbackQuery) -> None:
provider = (callback.data or "").split(":")[-1]
label = {
"cryptobot": "Crypto Bot",
"heleket": "Heleket",
"lava": "Lava",
"anypay": "AnyPay",
}.get(provider, provider)
await callback.answer()
if callback.message:
await callback.message.edit_text(
f"<b>🪙 Пополнение через {label}</b>\n\nВыбери сумму в рублях.",
reply_markup=topup_amounts_inline(provider),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("pay:topup_amount:"))
async def cb_pay_topup_amount(
callback: CallbackQuery,
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
anypay: Anypay,
) -> None:
user = callback.from_user
if not user:
await callback.answer()
return
parts = (callback.data or "").split(":")
if len(parts) >= 4:
provider = parts[2]
amount = float(parts[3])
else:
provider = "cryptobot"
amount = float(parts[-1])
if amount < 10 or amount > 3000:
await callback.answer("Сумма от 10 до 3000 ₽", show_alert=True)
return
await callback.answer("Создаю счёт…")
try:
if provider == "lava":
if not lava.enabled:
await callback.answer("Lava не настроен", show_alert=True)
return
order_id = _safe_order_id(f"topup_{user.id}_{int(amount)}_{int(time.time())}")
inv = await lava.create_invoice(
amount_rub=amount,
order_id=order_id,
comment=f"Topup {int(amount)} RUB tg={user.id}",
custom_fields=str(user.id),
)
local_id = db.next_local_invoice_id()
db.create_invoice_record(
telegram_id=user.id,
invoice_id=local_id,
kind="topup",
amount_rub=amount,
pay_url=inv.pay_url,
payload=None,
provider="lava",
order_id=order_id,
external_uuid=inv.invoice_id,
)
if callback.message:
await callback.message.edit_text(
f"<b>💳 Счёт Lava на {amount:.0f} ₽</b>\n\n"
"Оплати картой или СБП. Баланс зачислится автоматически.",
reply_markup=invoice_inline(local_id, inv.pay_url, provider="lava"),
parse_mode="HTML",
)
return
if provider == "anypay":
if not anypay.enabled:
await callback.answer("AnyPay не настроен", show_alert=True)
return
local_id = db.next_local_invoice_id()
pay_id = str(local_id)
inv = await anypay.create_invoice(
pay_id=pay_id,
amount_rub=amount,
desc=f"Topup {int(amount)} RUB tg={user.id}",
)
db.create_invoice_record(
telegram_id=user.id,
invoice_id=local_id,
kind="topup",
amount_rub=amount,
pay_url=inv.pay_url,
payload=pay_id,
provider="anypay",
order_id=pay_id,
external_uuid=inv.transaction_id,
)
if callback.message:
await callback.message.edit_text(
f"<b>💳 Счёт AnyPay на {amount:.0f} ₽</b>\n\n"
"Оплати на странице AnyPay. Баланс зачислится автоматически.",
reply_markup=invoice_inline(local_id, inv.pay_url, provider="anypay"),
parse_mode="HTML",
)
return
if provider == "heleket":
if not heleket.enabled:
await callback.answer("Heleket не настроен", show_alert=True)
return
order_id = _safe_order_id(f"topup_{user.id}_{int(amount)}_{int(time.time())}")
inv = await heleket.create_invoice(
amount_rub=amount,
order_id=order_id,
description=f"Topup {int(amount)} RUB tg={user.id}",
)
local_id = db.next_local_invoice_id()
db.create_invoice_record(
telegram_id=user.id,
invoice_id=local_id,
kind="topup",
amount_rub=amount,
pay_url=inv.pay_url,
payload=order_id,
provider="heleket",
order_id=order_id,
external_uuid=inv.uuid,
)
pay_url = inv.pay_url
invoice_id = local_id
how = "Heleket"
else:
if not cryptopay.enabled:
await callback.answer("Crypto Bot не настроен", show_alert=True)
return
payload = f"topup:{user.id}:{int(amount)}"
invoice = await cryptopay.create_fiat_invoice(
amount_rub=amount,
description=f"Пополнение баланса на {int(amount)} ₽",
payload=payload,
)
db.create_invoice_record(
telegram_id=user.id,
invoice_id=invoice.invoice_id,
kind="topup",
amount_rub=amount,
pay_url=invoice.pay_url,
payload=payload,
provider="cryptobot",
)
pay_url = invoice.pay_url
invoice_id = invoice.invoice_id
how = "Crypto Bot"
except (CryptoPayError, HeleketError, AnypayError) as exc:
if callback.message:
await callback.message.edit_text(
f"Не удалось создать счёт.\n<code>{exc}</code>",
parse_mode="HTML",
)
return
if callback.message:
await callback.message.edit_text(
f"<b>🪙 Счёт на {int(amount)} ₽</b> ({how})\n\n"
"1) Нажми «Оплатить»\n"
"2) Оплати криптой\n"
"3) Вернись и нажми «Я оплатил»",
reply_markup=invoice_inline(invoice_id, pay_url, provider=provider),
parse_mode="HTML",
)
@router.message(F.text == "🛒 Тарифы")
async def cmd_tariffs(message: Message, tariffs: list[Tariff]) -> None:
await message.answer(
"<b>🛒 Тарифы</b>\n\nВыбери подходящий план:",
reply_markup=tariffs_inline(tariffs),
parse_mode="HTML",
)
@router.callback_query(F.data == "shop:list")
async def cb_shop_list(callback: CallbackQuery, tariffs: list[Tariff]) -> None:
await callback.answer()
if callback.message:
await callback.message.edit_text(
"<b>🛒 Тарифы</b>\n\nВыбери подходящий план:",
reply_markup=tariffs_inline(tariffs),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("shop:view:"))
async def cb_shop_view(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
) -> None:
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
balance = db.get_balance(user.id) if user else 0
await callback.answer()
if callback.message:
await callback.message.edit_text(
_tariff_card(tariff, balance),
reply_markup=_pay_kb(cryptopay, heleket, tariff_id=tariff.id, lava=lava),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("shop:buy_balance:"))
async def cb_shop_buy_balance(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
sdk: RemnawaveSDK,
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
) -> None:
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer("Ошибка пользователя", show_alert=True)
return
await callback.answer("Оформляю…")
db.ensure_user(user.id, username=user.username, full_name=user.full_name)
result = await purchase_tariff(
db=db,
sdk=sdk,
telegram_id=user.id,
tariff=tariff,
charge_balance=True,
)
if callback.message:
await callback.message.edit_text(
result.message,
reply_markup=(
_pay_kb(cryptopay, heleket, lava=lava)
if result.ok
else _pay_kb(cryptopay, heleket, tariff_id=tariff.id, lava=lava)
),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("shop:buy_crypto:"))
async def cb_shop_buy_crypto(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
cryptopay: CryptoPay,
) -> None:
if not cryptopay.enabled:
await callback.answer("Крипто-оплата не настроена", show_alert=True)
return
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer()
return
await callback.answer("Создаю счёт…")
payload = f"tariff:{tariff.id}:{user.id}"
try:
invoice = await cryptopay.create_fiat_invoice(
amount_rub=tariff.price,
description=f"Тариф «{tariff.title}» — {tariff.format_price()}",
payload=payload,
)
except CryptoPayError as exc:
if callback.message:
await callback.message.edit_text(
f"Не удалось создать счёт.\n<code>{exc}</code>",
parse_mode="HTML",
)
return
db.create_invoice_record(
telegram_id=user.id,
invoice_id=invoice.invoice_id,
kind="tariff",
amount_rub=tariff.price,
pay_url=invoice.pay_url,
payload=payload,
tariff_id=tariff.id,
provider="cryptobot",
)
if callback.message:
await callback.message.edit_text(
f"<b>🪙 Оплата тарифа «{tariff.title}»</b> (Crypto Bot)\n"
f"Сумма: <b>{tariff.format_price()}</b>\n\n"
"1) Нажми «Оплатить»\n"
"2) Оплати в Crypto Bot\n"
"3) Вернись и нажми «Я оплатил»",
reply_markup=invoice_inline(
invoice.invoice_id, invoice.pay_url, provider="cryptobot"
),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("shop:buy_heleket:"))
async def cb_shop_buy_heleket(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
heleket: Heleket,
) -> None:
if not heleket.enabled:
await callback.answer("Heleket не настроен", show_alert=True)
return
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer()
return
await callback.answer("Создаю счёт…")
order_id = _safe_order_id(
f"tariff_{tariff.id}_{user.id}_{int(time.time())}_{uuid.uuid4().hex[:6]}"
)
try:
inv = await heleket.create_invoice(
amount_rub=tariff.price,
order_id=order_id,
description=f"Tariff {tariff.id} tg={user.id}",
)
except HeleketError as exc:
if callback.message:
await callback.message.edit_text(
f"Не удалось создать счёт Heleket.\n<code>{exc}</code>",
parse_mode="HTML",
)
return
local_id = db.next_local_invoice_id()
db.create_invoice_record(
telegram_id=user.id,
invoice_id=local_id,
kind="tariff",
amount_rub=tariff.price,
pay_url=inv.pay_url,
payload=order_id,
tariff_id=tariff.id,
provider="heleket",
order_id=order_id,
external_uuid=inv.uuid,
)
if callback.message:
await callback.message.edit_text(
f"<b>🪙 Оплата тарифа «{tariff.title}»</b> (Heleket)\n"
f"Сумма: <b>{tariff.format_price()}</b>\n\n"
"1) Нажми «Оплатить»\n"
"2) Оплати на странице Heleket\n"
"3) Вернись и нажми «Я оплатил» "
"(или дождись авто-зачисления)",
reply_markup=invoice_inline(local_id, inv.pay_url, provider="heleket"),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("pay:check:"))
async def cb_pay_check(
callback: CallbackQuery,
db: Database,
sdk: RemnawaveSDK,
tariffs: list[Tariff],
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
) -> None:
if not cryptopay.enabled:
await callback.answer("Крипто-оплата не настроена", show_alert=True)
return
invoice_id = int((callback.data or "").split(":")[-1])
local = db.get_invoice(invoice_id, provider="cryptobot")
if not local:
await callback.answer("Счёт не найден", show_alert=True)
return
if local["telegram_id"] != (callback.from_user.id if callback.from_user else 0):
await callback.answer("Это не твой счёт", show_alert=True)
return
await callback.answer("Проверяю…")
try:
remote = await cryptopay.get_invoice(invoice_id)
except CryptoPayError as exc:
if callback.message:
await callback.message.answer(
f"Ошибка проверки: <code>{exc}</code>", parse_mode="HTML"
)
return
if remote is None or remote.status != "paid":
status = remote.status if remote else "unknown"
await callback.answer(f"Ещё не оплачено ({status})", show_alert=True)
return
result = await fulfill_paid_invoice(db=db, sdk=sdk, tariffs=tariffs, local=local)
if callback.message:
await callback.message.edit_text(
result.message,
reply_markup=_pay_kb(cryptopay, heleket, lava=lava),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("pay:check_hk:"))
async def cb_pay_check_heleket(
callback: CallbackQuery,
db: Database,
sdk: RemnawaveSDK,
tariffs: list[Tariff],
cryptopay: CryptoPay,
heleket: Heleket,
lava: Lava,
) -> None:
if not heleket.enabled:
await callback.answer("Heleket не настроен", show_alert=True)
return
invoice_id = int((callback.data or "").split(":")[-1])
local = db.get_invoice(invoice_id, provider="heleket")
if not local:
await callback.answer("Счёт не найден", show_alert=True)
return
if local["telegram_id"] != (callback.from_user.id if callback.from_user else 0):
await callback.answer("Это не твой счёт", show_alert=True)
return
await callback.answer("Проверяю…")
order_id = str(local.get("order_id") or "")
try:
remote = await heleket.get_invoice(
order_id=order_id or None,
uuid=str(local.get("external_uuid") or "") or None,
)
except HeleketError as exc:
if callback.message:
await callback.message.answer(
f"Ошибка проверки: <code>{exc}</code>", parse_mode="HTML"
)
return
if remote is None or not Heleket.is_paid(remote.status):
status = remote.status if remote else "unknown"
await callback.answer(f"Ещё не оплачено ({status})", show_alert=True)
return
result = await fulfill_paid_invoice(db=db, sdk=sdk, tariffs=tariffs, local=local)
if callback.message:
await callback.message.edit_text(
result.message,
reply_markup=_pay_kb(cryptopay, heleket, lava=lava),
parse_mode="HTML",
)
@router.callback_query(F.data == "traffic:list")
async def cb_traffic_list(
callback: CallbackQuery,
sdk: RemnawaveSDK,
db: Database,
tariffs: list[Tariff],
) -> None:
user = callback.from_user
if not user:
await callback.answer()
return
info = await get_my_subscription_info(sdk, user.id, db=db, tariffs=tariffs)
if not info.has_subscription:
await callback.answer("Сначала купи тариф", show_alert=True)
return
balance = db.get_balance(user.id)
traffic_line = info.traffic.format_line() if info.traffic else "—"
await callback.answer()
if callback.message:
await callback.message.edit_text(
"<b>📦 Докупка трафика</b>\n\n"
f"{traffic_line}\n"
f"Баланс: <b>{balance:.0f} ₽</b>\n\n"
f"База в тарифе: <b>{format_gb(BASE_TRAFFIC_GB, digits=0)}</b>\n"
f"Пакет: <b>+{format_gb(PACK_GB, digits=0)} = {int(PACK_PRICE_RUB)} ₽/мес</b>\n\n"
"⚠️ Доп. трафик оплачивается <b>ежемесячно</b> с баланса "
"(первый месяц — при покупке).\n\n"
"Выбери объём:",
reply_markup=traffic_packs_inline(),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("traffic:buy:"))
async def cb_traffic_buy_view(
callback: CallbackQuery,
db: Database,
) -> None:
packs = int((callback.data or "").split(":")[-1])
pack = get_traffic_pack(str(packs))
if not pack:
await callback.answer("Пакет не найден", show_alert=True)
return
user = callback.from_user
balance = db.get_balance(user.id) if user else 0
await callback.answer()
if callback.message:
await callback.message.edit_text(
f"<b>📦 {pack.title}</b>\n\n"
f"Добавится к текущему лимиту.\n"
f"Цена: <b>{pack.format_price()}</b> (первый месяц)\n"
f"Далее списание <b>каждые 30 дней</b> с баланса.\n"
f"Баланс: <b>{balance:.0f} ₽</b>",
reply_markup=traffic_confirm_inline(pack.packs),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("traffic:confirm:"))
async def cb_traffic_confirm(
callback: CallbackQuery,
db: Database,
sdk: RemnawaveSDK,
) -> None:
packs = int((callback.data or "").split(":")[-1])
pack = get_traffic_pack(str(packs))
if not pack:
await callback.answer("Пакет не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer()
return
await callback.answer("Покупаю…")
result = await purchase_traffic_pack(
db=db,
sdk=sdk,
telegram_id=user.id,
pack=pack,
charge_balance=True,
)
if callback.message:
await callback.message.edit_text(
result.message,
reply_markup=(
subscription_inline(has_subscription=True)
if result.ok
else traffic_confirm_inline(pack.packs)
),
parse_mode="HTML",
)
@router.callback_query(F.data == "devices:list")
async def cb_devices_list(
callback: CallbackQuery,
sdk: RemnawaveSDK,
db: Database,
tariffs: list[Tariff],
) -> None:
user = callback.from_user
if not user:
await callback.answer()
return
info = await get_my_subscription_info(sdk, user.id, db=db, tariffs=tariffs)
if not info.has_subscription:
await callback.answer("Сначала купи тариф", show_alert=True)
return
balance = db.get_balance(user.id)
devices = info.devices
devices_line = devices.format_line() if devices else "—"
excess = devices.excess if devices else 0
await callback.answer()
if callback.message:
await callback.message.edit_text(
"<b>📱 Устройства</b>\n\n"
f"{devices_line}\n"
f"Баланс: <b>{balance:.0f} ₽</b>\n\n"
f"База: <b>{BASE_DEVICES}</b> устройств в тарифе.\n"
f"Доп. слот: <b>{int(EXTRA_DEVICE_PRICE_RUB)} ₽/мес</b> "
f"(первый месяц при покупке).\n"
"⚠️ Списание с баланса <b>каждые 30 дней</b> за все доп. устройства.\n"
"При превышении HWID подписка отключается до оплаты слотов.",
reply_markup=device_packs_inline(excess=excess),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("devices:buy:"))
async def cb_devices_buy_view(
callback: CallbackQuery,
db: Database,
) -> None:
slots = int((callback.data or "").split(":")[-1])
pack = get_device_pack(str(slots))
if not pack:
await callback.answer("Пакет не найден", show_alert=True)
return
user = callback.from_user
balance = db.get_balance(user.id) if user else 0
await callback.answer()
if callback.message:
await callback.message.edit_text(
f"<b>📱 {pack.title}</b>\n\n"
f"Добавится к лимиту устройств.\n"
f"Цена: <b>{pack.format_price()}</b> (первый месяц)\n"
f"Далее — <b>{int(EXTRA_DEVICE_PRICE_RUB)} ₽/мес</b> за слот с баланса.\n"
f"Баланс: <b>{balance:.0f} ₽</b>",
reply_markup=device_confirm_inline(pack.slots),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("devices:confirm:"))
async def cb_devices_confirm(
callback: CallbackQuery,
db: Database,
sdk: RemnawaveSDK,
) -> None:
slots = int((callback.data or "").split(":")[-1])
pack = get_device_pack(str(slots))
if not pack:
# оплата произвольного превышения excess N
from services.devices import DevicePack, EXTRA_DEVICE_PRICE_RUB as PRICE
pack = DevicePack(slots=slots, price=PRICE * slots)
user = callback.from_user
if not user:
await callback.answer()
return
await callback.answer("Покупаю…")
result = await purchase_device_pack(
db=db,
sdk=sdk,
telegram_id=user.id,
pack=pack,
charge_balance=True,
)
if callback.message:
over = bool(result.devices and result.devices.over_limit)
await callback.message.edit_text(
result.message,
reply_markup=(
subscription_inline(has_subscription=True, devices_over=over)
if result.ok
else device_confirm_inline(pack.slots)
),
parse_mode="HTML",
)
@router.message(F.text == "📱 Моя подписка")
async def cmd_my_sub(
message: Message, sdk: RemnawaveSDK, db: Database, tariffs: list[Tariff]
) -> None:
user = message.from_user
if not user:
return
db.ensure_user(user.id, username=user.username, full_name=user.full_name)
wait = await message.answer("⏳ Загружаю…")
try:
info = await get_my_subscription_info(
sdk,
user.id,
db=db,
tariffs=tariffs,
renewal_bot=message.bot,
)
await wait.edit_text(
info.text,
reply_markup=subscription_inline(
has_subscription=info.has_subscription,
devices_over=bool(info.devices and info.devices.over_limit),
),
parse_mode="HTML",
)
except Exception as exc: # noqa: BLE001
await wait.edit_text(
f"Не удалось получить подписку.\n<code>{exc}</code>",
parse_mode="HTML",
)
@router.callback_query(F.data == "sub:mine")
async def cb_my_sub(
callback: CallbackQuery,
sdk: RemnawaveSDK,
db: Database,
tariffs: list[Tariff],
) -> None:
user = callback.from_user
if not user:
await callback.answer()
return
await callback.answer()
try:
info = await get_my_subscription_info(
sdk,
user.id,
db=db,
tariffs=tariffs,
renewal_bot=callback.bot,
)
if callback.message:
await callback.message.edit_text(
info.text,
reply_markup=subscription_inline(
has_subscription=info.has_subscription,
devices_over=bool(info.devices and info.devices.over_limit),
),
parse_mode="HTML",
)
except Exception as exc: # noqa: BLE001
if callback.message:
await callback.message.edit_text(
f"Не удалось получить подписку.\n<code>{exc}</code>",
parse_mode="HTML",
)
@router.callback_query(F.data == "renew:list")
async def cb_renew_list(
callback: CallbackQuery,
tariffs: list[Tariff],
sdk: RemnawaveSDK,
db: Database,
) -> None:
user = callback.from_user
if not user:
await callback.answer()
return
info = await get_my_subscription_info(sdk, user.id, db=db, tariffs=tariffs)
if not info.has_subscription:
await callback.answer("Сначала купи тариф", show_alert=True)
if callback.message:
await callback.message.edit_text(
info.text,
reply_markup=subscription_inline(has_subscription=False),
parse_mode="HTML",
)
return
balance = db.get_balance(user.id)
await callback.answer()
expire = (
info.expire_at.strftime("%d.%m.%Y %H:%M UTC")
if info.expire_at
else "—"
)
if callback.message:
await callback.message.edit_text(
"<b>🔄 Продление подписки</b>\n\n"
f"Сейчас до: <b>{expire}</b>\n"
f"Баланс: <b>{balance:.0f} ₽</b>\n\n"
"Выбери срок — дни добавятся к текущей дате.\n"
"Оплата только с баланса.",
reply_markup=renew_tariffs_inline(tariffs),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("renew:view:"))
async def cb_renew_view(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
sdk: RemnawaveSDK,
) -> None:
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
balance = db.get_balance(user.id) if user else 0
info = await get_my_subscription_info(sdk, user.id, db=db, tariffs=tariffs) if user else None
expire = (
info.expire_at.strftime("%d.%m.%Y")
if info and info.expire_at
else "—"
)
await callback.answer()
if callback.message:
await callback.message.edit_text(
f"<b>🔄 Продлить: {tariff.title}</b>\n\n"
f"{tariff.description}\n\n"
f" Срок: <b>+{tariff.days} дн.</b>\n"
f"💵 Цена: <b>{tariff.format_price()}</b>\n"
f"📅 Сейчас до: <b>{expire}</b>\n"
f"💰 Баланс: <b>{balance:.0f} ₽</b>",
reply_markup=renew_confirm_inline(tariff.id),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("renew:buy:"))
async def cb_renew_buy(
callback: CallbackQuery,
tariffs: list[Tariff],
db: Database,
sdk: RemnawaveSDK,
) -> None:
tariff_id = (callback.data or "").split(":", 2)[2]
tariff = get_tariff(tariffs, tariff_id)
if not tariff:
await callback.answer("Тариф не найден", show_alert=True)
return
user = callback.from_user
if not user:
await callback.answer("Ошибка пользователя", show_alert=True)
return
await callback.answer("Продлеваю…")
db.ensure_user(user.id, username=user.username, full_name=user.full_name)
result = await purchase_tariff(
db=db,
sdk=sdk,
telegram_id=user.id,
tariff=tariff,
charge_balance=True,
renew=True,
payment_meta=f"renew:{tariff.id}",
)
if callback.message:
await callback.message.edit_text(
result.message,
reply_markup=(
subscription_inline(has_subscription=True)
if result.ok
else renew_confirm_inline(tariff.id)
),
parse_mode="HTML",
)