1724 lines
59 KiB
Python
1724 lines
59 KiB
Python
"""Баланс, тарифы, оплата с баланса и криптой."""
|
||
|
||
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",
|
||
)
|