diff --git a/handlers/profile.py b/handlers/profile.py
index 8a5061d..c1ef4bb 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -1,7 +1,12 @@
# Профиль. последнее изменение 24.11.2025
-from aiogram import Router, types
-from aiogram.types import CallbackQuery
+from aiogram import Router, types, F
+from aiogram.types import (
+ CallbackQuery,
+ LabeledPrice,
+ Message,
+ PreCheckoutQuery,
+)
import logging
from datetime import datetime
from aiogram.enums.parse_mode import ParseMode
@@ -233,17 +238,54 @@ async def popup_confirm_callback_handler(callback: CallbackQuery):
await callback.answer()
+# ===== Telegram Stars =====
+
@router.callback_query(lambda callback: callback.data.startswith("method_stars_"))
async def method_stars_handler(callback: CallbackQuery):
"""
- Заглушка: оплата через Telegram Stars.
+ Оплата через Telegram Stars.
+ Формируем invoice прямо из бота, без отдельного биллинга.
"""
- amount = callback.data.split("_")[-1]
- await callback.message.edit_text(
- f"⭐ Оплата через Telegram Stars на {amount} ₽ пока в разработке.\n\n"
- "Позже сюда подвяжем реальный платёж.",
- )
- await callback.answer()
+ try:
+ amount_str = callback.data.split("_")[-1]
+ amount_rub = int(float(amount_str))
+ except Exception:
+ await callback.message.answer("Некорректная сумма для оплаты.")
+ await callback.answer()
+ return
+
+ # Внутренний payload, чтобы при успешной оплате понять, что это пополнение баланса
+ payload = f"stars_topup:{callback.from_user.id}:{amount_rub}"
+
+ # 1 ₽ == 1 Star, если нужна другая конвертация — меняй тут
+ stars_amount = amount_rub
+
+ prices = [
+ LabeledPrice(
+ label=f"Пополнение баланса на {amount_rub} ₽",
+ amount=stars_amount,
+ )
+ ]
+
+ try:
+ await callback.message.answer_invoice(
+ title="Пополнение баланса Lark VPN",
+ description=(
+ f"Пополнение баланса на {amount_rub} ₽ через Telegram Stars.\n\n"
+ "После успешной оплаты баланс будет зачислен автоматически."
+ ),
+ payload=payload,
+ provider_token="", # для Stars провайдер пустой
+ currency="XTR",
+ prices=prices,
+ )
+ await callback.answer()
+ except Exception as e:
+ logger.exception(f"Ошибка при отправке invoice Telegram Stars: {e}")
+ await callback.message.answer(
+ "Не удалось создать счёт в Telegram Stars. Попробуй позже или выбери другой способ оплаты."
+ )
+ await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("method_ykassa_"))
@@ -383,3 +425,87 @@ async def guide_callback_handler(callback: CallbackQuery):
reply_markup=guide_keyboard(),
)
await callback.answer()
+
+
+# ===== Служебные хендлеры для платежей Telegram Stars =====
+
+@router.pre_checkout_query()
+async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery):
+ """
+ Обязательный шаг для Telegram Payments:
+ подтверждаем pre_checkout_query, иначе платёж не пройдёт.
+ """
+ try:
+ await pre_checkout_query.bot.answer_pre_checkout_query(
+ pre_checkout_query.id,
+ ok=True,
+ )
+ except Exception as e:
+ logger.exception(f"Ошибка при answer_pre_checkout_query: {e}")
+
+
+async def _process_stars_topup(message: Message):
+ """
+ Логика зачисления средств после успешной оплаты Stars.
+ """
+ sp = message.successful_payment
+ if not sp:
+ return
+
+ payload = sp.invoice_payload or ""
+ parts = payload.split(":")
+ if len(parts) != 3 or parts[0] != "stars_topup":
+ logger.info(
+ "successful_payment не относится к пополнению баланса Stars.")
+ return
+
+ _, telegram_id_str, amount_str = parts
+
+ try:
+ amount_rub = int(amount_str)
+ except ValueError:
+ # На всякий случай fallback к total_amount
+ amount_rub = sp.total_amount
+
+ data = {
+ "telegram_id": telegram_id_str,
+ "amount": amount_rub,
+ "currency": sp.currency,
+ "provider": "telegram_stars",
+ "telegram_payment_charge_id": sp.telegram_payment_charge_id,
+ }
+
+ logger.info(
+ f"Обработка успешного платежа Telegram Stars: "
+ f"user={telegram_id_str}, amount={amount_rub}, currency={sp.currency}"
+ )
+
+ result = await call_api("POST", "/user/deposit", data)
+
+ if result == "ERROR" or result is None:
+ await message.answer(
+ "⭐ Оплата через Telegram Stars прошла, но не удалось автоматически обновить баланс.\n"
+ "Если баланс не изменился — напиши, пожалуйста, в поддержку и укажи время платежа."
+ )
+ return
+
+ await message.answer(
+ f"⭐ Оплата через Telegram Stars успешно проведена.\n"
+ f"На твой баланс зачислено {amount_rub} ₽."
+ )
+
+
+@router.message(F.successful_payment)
+async def successful_payment_handler(message: Message):
+ """
+ Глобальный хендлер успешных платежей.
+ Сейчас используем только для пополнения баланса через Stars.
+ """
+ try:
+ await _process_stars_topup(message)
+ except Exception as e:
+ logger.exception(f"Ошибка при обработке successful_payment: {e}")
+ await message.answer(
+ "Оплата прошла, но произошла ошибка при обработке. "
+ "Если баланс не изменился — напиши, пожалуйста, в поддержку."
+ )
diff --git a/handlers/referrals.py b/handlers/referrals.py
index 283891a..0a470aa 100644
--- a/handlers/referrals.py
+++ b/handlers/referrals.py
@@ -2,25 +2,118 @@ from aiogram import Router, types
from aiogram.filters import Command
from aiogram.enums.parse_mode import ParseMode
import logging
+import aiohttp
+
+from instences.config import BASE_URL_FASTAPI
router = Router()
logger = logging.getLogger(__name__)
+async def call_api(method, endpoint, data=None, base_url=BASE_URL_FASTAPI):
+ """
+ Универсальный HTTP-запрос к FastAPI для рефералок.
+
+ Ожидаем от бекенда:
+ GET /user/{telegram_id}/referrals -> {
+ "invited_count": int,
+ "bonus_total": float
+ }
+ """
+ url = f"{base_url}{endpoint}"
+ logger.info(
+ f"[referrals] Инициализация запроса: {method} {url} с данными {data}")
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.request(
+ method,
+ url,
+ json=data,
+ headers={"Content-Type": "application/json"},
+ ) as response:
+ logger.info(
+ f"[referrals] Получен ответ от {url}: статус {response.status}"
+ )
+
+ if response.status in {200, 201}:
+ result = await response.json()
+ logger.debug(f"[referrals] Ответ JSON: {result}")
+ return result
+ if response.status == 404:
+ logger.debug(
+ f"[referrals] Код {response.status}, возвращаю None"
+ )
+ return None
+ logger.error(
+ f"[referrals] Ошибка в запросе: статус {response.status}, причина {response.reason}"
+ )
+ return "ERROR"
+ except Exception as e:
+ logger.exception(
+ f"[referrals] Исключение при выполнении запроса к {url}: {e}")
+ return "ERROR"
+
+
async def _build_referral_text(bot, user_id: int) -> str:
+ """
+ Текст реферальной программы + статистика, если есть.
+ """
me = await bot.get_me()
bot_username = me.username or "LarkVPN_bot"
link = f"https://t.me/{bot_username}?start=ref_{user_id}"
+ # Базовый текст: без фейковых тире и нулевых рублей
text = (
"👥 Реферальная программа\n\n"
"Зови друзей в Lark VPN и получай бонусы на баланс.\n\n"
f"🔗 Твоя ссылка:\n{link}\n\n"
- "👤 Приглашено: —\n"
- "💰 Начислено бонусов: — ₽\n\n"
+ )
+
+ invited_line = ""
+ bonus_line = ""
+
+ stats = await call_api("GET", f"/user/{user_id}/referrals")
+ if isinstance(stats, dict):
+ invited = stats.get("invited_count")
+ bonus = stats.get("bonus_total")
+
+ if invited is not None:
+ try:
+ invited_int = int(invited)
+ except (TypeError, ValueError):
+ invited_int = None
+
+ if invited_int is not None:
+ invited_line = f"👤 Приглашено: {invited_int}\n"
+
+ if bonus is not None:
+ try:
+ bonus_val = float(bonus)
+ except (TypeError, ValueError):
+ bonus_val = None
+
+ # Строку с бонусами показываем только если реально что-то начислено
+ if bonus_val is not None and bonus_val > 0:
+ bonus_line = f"💰 Начислено бонусов: {bonus_val:.2f} ₽\n"
+ else:
+ logger.warning(
+ f"[referrals] Не удалось получить статистику для user_id={user_id}: {stats}"
+ )
+
+ if invited_line:
+ text += invited_line
+ if bonus_line:
+ text += bonus_line
+
+ if invited_line or bonus_line:
+ text += "\n"
+
+ text += (
"Бонусы падают автоматически, когда приглашённые пополняют баланс."
)
+
return text
@@ -41,7 +134,7 @@ async def referrals_command(message: types.Message):
@router.callback_query(lambda callback: callback.data == "referral")
async def referrals_callback(callback: types.CallbackQuery):
"""
- Кнопка «Реферальная программа» в главном меню.
+ Кнопка «Реферальная программа» (если где-то есть).
"""
try:
text = await _build_referral_text(
diff --git a/handlers/start.py b/handlers/start.py
index 69c20eb..9570b62 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -54,19 +54,49 @@ def _welcome_text(username: str | None) -> str:
return "🥚 Lark Security\n\nВыберите действие из меню ниже."
+def _parse_referrer_id(message: Message) -> int | None:
+ """
+ Достаём ref_ из /start.
+ Примеры:
+ /start
+ /start ref_123456789
+ """
+ text = message.text or ""
+ parts = text.split(maxsplit=1)
+ if len(parts) < 2:
+ return None
+
+ arg = parts[1].strip()
+ if not arg.startswith("ref_"):
+ return None
+
+ raw_id = arg[4:]
+ if not raw_id.isdigit():
+ return None
+
+ try:
+ return int(raw_id)
+ except ValueError:
+ return None
+
+
@router.message(Command("start"))
async def start_command(message: Message):
"""
Обработчик команды /start.
- Визуал и текст — обновлены, логика работы с API не тронута.
+ Добавлена обработка реферального payload'а ref_.
"""
logger.info(
f"Получена команда /start от пользователя: "
- f"{message.from_user.id} ({message.from_user.username})"
+ f"{message.from_user.id} ({message.from_user.username}) | text='{message.text}'"
)
+ user_id = message.from_user.id
+ referrer_id = _parse_referrer_id(message)
+
try:
- user_data = await call_api("GET", f"/user/{message.from_user.id}")
+ # 1. Проверяем/создаём пользователя в базе
+ user_data = await call_api("GET", f"/user/{user_id}")
if not user_data:
logger.debug(
"Пользователь не найден в базе, создаем новую запись."
@@ -74,9 +104,33 @@ async def start_command(message: Message):
await call_api(
"POST",
"/user/create",
- {"telegram_id": message.from_user.id},
+ {"telegram_id": user_id},
)
+ # 2. Если есть реферер и он не сам пользователь — отправляем инфу в бекенд
+ if referrer_id and referrer_id != user_id:
+ payload = {
+ "referrer_id": referrer_id,
+ "telegram_id": user_id,
+ }
+ logger.info(
+ f"Отправка данных о реферале в бекенд: {payload}"
+ )
+ result = await call_api(
+ "POST",
+ "/user/referrals/track",
+ payload,
+ )
+ if result == "ERROR":
+ logger.error(
+ f"Не удалось зафиксировать реферала в бекенде: {payload}"
+ )
+ elif referrer_id == user_id:
+ logger.info(
+ "Обнаружена попытка самореферала, запрос в бекенд не отправляем."
+ )
+
+ # 3. Приветственное сообщение и главное меню
logger.debug("Отправка приветственного сообщения пользователю.")
await message.answer(
_welcome_text(message.from_user.username),
@@ -86,7 +140,7 @@ async def start_command(message: Message):
except Exception as e:
logger.exception(
f"Ошибка при обработке команды /start для пользователя "
- f"{message.from_user.id}: {e}"
+ f"{user_id}: {e}"
)
await message.answer("Произошла ошибка. Попробуйте позже.")