diff --git a/tg_bot/infrastructure/telegram/bot.py b/tg_bot/infrastructure/telegram/bot.py
index 55596c6..fee0fde 100644
--- a/tg_bot/infrastructure/telegram/bot.py
+++ b/tg_bot/infrastructure/telegram/bot.py
@@ -8,7 +8,8 @@ from tg_bot.infrastructure.telegram.handlers import (
start_handler,
help_handler,
stats_handler,
- question_handler
+ question_handler,
+ buy_handler
)
logger = logging.getLogger(__name__)
@@ -24,6 +25,7 @@ async def create_bot() -> tuple[Bot, Dispatcher]:
dp.include_router(help_handler.router)
dp.include_router(stats_handler.router)
dp.include_router(question_handler.router)
+ dp.include_router(buy_handler.router)
return bot, dp
diff --git a/tg_bot/infrastructure/telegram/handlers/buy_handler.py b/tg_bot/infrastructure/telegram/handlers/buy_handler.py
new file mode 100644
index 0000000..444ffd9
--- /dev/null
+++ b/tg_bot/infrastructure/telegram/handlers/buy_handler.py
@@ -0,0 +1,283 @@
+from aiogram import Router, types
+from aiogram.filters import Command
+from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
+from decimal import Decimal
+from tg_bot.config.settings import settings
+from tg_bot.payment.yookassa.client import yookassa_client
+from tg_bot.infrastructure.database.database import SessionLocal
+from tg_bot.infrastructure.database.models import PaymentModel, UserModel
+import uuid
+from datetime import datetime, timedelta
+
+router = Router()
+
+
+@router.message(Command("buy"))
+async def cmd_buy(message: Message):
+ user_id = message.from_user.id
+ username = message.from_user.username or f"user_{user_id}"
+
+ session = SessionLocal()
+ try:
+ user = session.query(UserModel).filter_by(
+ telegram_id=str(user_id)
+ ).first()
+
+ if user and user.is_premium and user.premium_until and user.premium_until > datetime.now():
+ days_left = (user.premium_until - datetime.now()).days
+ await message.answer(
+ f"У вас уже есть активная подписка!\n\n"
+ f"• Статус: Premium активен\n"
+ f"• Действует до: {user.premium_until.strftime('%d.%m.%Y')}\n"
+ f"• Осталось дней: {days_left}\n\n"
+ f"Новая подписка будет добавлена к текущей.",
+ parse_mode="HTML"
+ )
+ finally:
+ session.close()
+
+ await message.answer(
+ "*Создаю ссылку для оплаты...*\n\n"
+ "Пожалуйста, подождите несколько секунд.",
+ parse_mode="Markdown"
+ )
+
+ try:
+ payment_data = await yookassa_client.create_payment(
+ amount=Decimal(str(settings.PAYMENT_AMOUNT)),
+ description=f"Подписка VibeLawyerBot для @{username}",
+ user_id=user_id
+ )
+
+ session = SessionLocal()
+ try:
+ payment = PaymentModel(
+ payment_id=str(uuid.uuid4()),
+ user_id=user_id,
+ amount=str(settings.PAYMENT_AMOUNT),
+ currency="RUB",
+ status="pending",
+ yookassa_payment_id=payment_data["id"],
+ description="Оплата подписки VibeLawyerBot"
+ )
+ session.add(payment)
+ session.commit()
+ print(f"Платёж сохранён в БД: {payment.payment_id}")
+ except Exception as e:
+ print(f"Ошибка сохранения платежа в БД: {e}")
+ session.rollback()
+ finally:
+ session.close()
+
+ keyboard = InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text="Оплатить онлайн",
+ url=payment_data["confirmation_url"]
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text="Проверить статус оплаты",
+ callback_data=f"check_payment:{payment_data['id']}"
+ )
+ ]
+ ]
+ )
+ response_text = (
+ f"Оплата подписки VibeLawyerBot\n\n"
+ f"Детали платежа:\n"
+ f"• Сумма: {settings.PAYMENT_AMOUNT} руб.\n"
+ f"• Описание: Подписка на 30 дней\n"
+ f"• ID платежа: {payment_data['id'][:20]}...\n\n"
+ f"Что даёт подписка:\n"
+ f"• Неограниченное число вопросов\n"
+ f"• Приоритетная обработка\n"
+ f"• Доступ ко всем функциям\n\n"
+ f"После оплаты доступ активируется автоматически в течение 1-2 минут."
+ )
+
+ await message.answer(
+ response_text,
+ parse_mode="HTML",
+ reply_markup=keyboard,
+ disable_web_page_preview=True
+ )
+ await message.answer(
+ "Инструкция по оплате:\n\n"
+ "1. Нажмите кнопку 'Оплатить онлайн'\n"
+ "2. Введите данные банковской карты\n"
+ "3. Подтвердите оплату\n"
+ "4. После успешной оплаты нажмите 'Проверить статус оплаты'\n\n"
+ "Тестовые карты для проверки:\n"
+ "• 5555 5555 5555 4477 - успешная оплата\n"
+ " Срок: любой будущий (напр. 12/30)\n"
+ " CVV: любые 3 цифры (напр. 123)\n\n"
+ "Это тестовые карты, реальные деньги не списываются!",
+ parse_mode="HTML"
+ )
+
+ except Exception as e:
+ print(f"Ошибка создания платежа: {e}")
+ await message.answer(
+ "Произошла ошибка при создании платежа\n\n"
+ "Пожалуйста, попробуйте позже или обратитесь к администратору.\n\n"
+ f"Ошибка: {str(e)[:100]}",
+ parse_mode="HTML"
+ )
+
+
+@router.callback_query(lambda c: c.data.startswith("check_payment:"))
+async def check_payment_status(callback_query: types.CallbackQuery):
+ yookassa_id = callback_query.data.split(":")[1]
+ user_id = callback_query.from_user.id
+
+ await callback_query.answer("Проверяю статус оплаты...")
+
+ try:
+ from yookassa import Payment as YooPayment
+ payment = YooPayment.find_one(yookassa_id)
+
+ if payment.status == "succeeded":
+ session = SessionLocal()
+ try:
+ db_payment = session.query(PaymentModel).filter_by(
+ yookassa_payment_id=yookassa_id
+ ).first()
+
+ if db_payment:
+ db_payment.status = "succeeded"
+ user = session.query(UserModel).filter_by(
+ telegram_id=str(user_id)
+ ).first()
+
+ if user:
+ user.is_premium = True
+ if user.premium_until and user.premium_until > datetime.now():
+ user.premium_until = user.premium_until + timedelta(days=30)
+ else:
+ user.premium_until = datetime.now() + timedelta(days=30)
+
+ session.commit()
+ user = session.query(UserModel).filter_by(
+ telegram_id=str(user_id)
+ ).first()
+
+ await callback_query.message.answer(
+ "Оплата подтверждена!\n\n"
+ f"Ваш premium-доступ активирован до: "
+ f"{user.premium_until.strftime('%d.%m.%Y')}\n\n"
+ "Теперь вы можете:\n"
+ "• Задавать неограниченное количество вопросов\n"
+ "• Получать приоритетные ответы\n"
+ "• Использовать все функции бота\n\n"
+ "Спасибо за покупку!",
+ parse_mode="HTML"
+ )
+ else:
+ await callback_query.message.answer(
+ "Платёж найден в ЮKассе, но не в нашей БД\n\n"
+ "Пожалуйста, обратитесь к администратору.",
+ parse_mode="HTML"
+ )
+ finally:
+ session.close()
+
+ elif payment.status == "pending":
+ await callback_query.message.answer(
+ "Платёж ещё не завершён\n\n"
+ "Если вы уже оплатили, пожалуйста, подождите 1-2 минуты "
+ "и проверьте статус снова.\n\n"
+ "Проверьте правильность данных карты:\n"
+ "• Срок действия должен быть будущим\n"
+ "• CVV - 3 цифры на обратной стороне карты",
+ parse_mode="HTML"
+ )
+ else:
+ await callback_query.message.answer(
+ f"Статус платежа: {payment.status}\n\n"
+ "Попробуйте оплатить ещё раз или обратитесь в поддержку.\n\n"
+ "Для теста используйте карту:\n"
+ "5555 5555 5555 4477\n"
+ "Срок: 12/30, CVV: 123",
+ parse_mode="HTML"
+ )
+
+ except Exception as e:
+ print(f"Ошибка проверки статуса: {e}")
+ await callback_query.message.answer(
+ "Не удалось проверить статус платежа\n\n"
+ "Попробуйте позже или обратитесь к администратору.",
+ parse_mode="HTML"
+ )
+
+
+@router.message(Command("mypayments"))
+async def cmd_my_payments(message: Message):
+ user_id = message.from_user.id
+
+ session = SessionLocal()
+ try:
+ payments = session.query(PaymentModel).filter_by(
+ user_id=user_id
+ ).order_by(PaymentModel.created_at.desc()).limit(10).all()
+
+ if not payments:
+ await message.answer(
+ "У вас пока нет платежей\n\n"
+ "Используйте команду /buy чтобы оформить подписку.",
+ parse_mode="HTML"
+ )
+ return
+
+ response = ["Ваши последние платежи:\n"]
+
+ for i, payment in enumerate(payments, 1):
+ status_text = "Успешно" if payment.status == "succeeded" else "Ожидание" if payment.status == "pending" else "Ошибка"
+ response.append(
+ f"\n{i}. {payment.amount} руб. ({status_text})\n"
+ f"Статус: {payment.status}\n"
+ f"Дата: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n"
+ f"ID: {payment.payment_id[:8]}..."
+ )
+
+ response.append("\n\nПолный доступ открывается после успешной оплаты")
+
+ await message.answer(
+ "\n".join(response),
+ parse_mode="HTML"
+ )
+
+ finally:
+ session.close()
+
+
+@router.message(Command("testcards"))
+async def cmd_testcards(message: Message):
+ testcards_text = (
+ f"Тестовые банковские карты для оплаты\n\n"
+
+ f"Для тестирования оплаты используйте:\n\n"
+
+ f"Карта для успешной оплаты:\n"
+ f"• Номер: 5555 5555 5555 4477\n"
+ f"• Срок действия: ЛЮБОЙ будущий (например: 12/30)\n"
+ f"• CVV код: ЛЮБЫЕ 3 цифры (например: 123)\n"
+ f"• Результат: Оплата пройдёт успешно\n\n"
+
+ f"Карта для отказа в оплате:\n"
+ f"• Номер: 5555 5555 5555 4445\n"
+ f"• Срок действия: ЛЮБОЙ будущий\n"
+ f"• CVV код: ЛЮБЫЕ 3 цифры\n"
+ f"• Результат: Оплата будет отклонена\n\n"
+
+ f"Важно:\n"
+ f"• Это тестовые карты, реальные деньги не списываются\n"
+ f"• Используются только для проверки работы оплаты\n"
+ f"• После успешной тестовой оплаты premium активируется\n\n"
+
+ f"Для оплаты подписки используйте команду /buy"
+ )
+
+ await message.answer(testcards_text, parse_mode="HTML")
diff --git a/tg_bot/payment/__init__.py b/tg_bot/payment/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tg_bot/payment/webhooks/__init__.py b/tg_bot/payment/webhooks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tg_bot/payment/webhooks/handler.py b/tg_bot/payment/webhooks/handler.py
new file mode 100644
index 0000000..6bf0c99
--- /dev/null
+++ b/tg_bot/payment/webhooks/handler.py
@@ -0,0 +1,70 @@
+import json
+from fastapi import APIRouter, Request, HTTPException
+from fastapi.responses import JSONResponse
+import sys
+import os
+from datetime import datetime, timedelta
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+router = APIRouter()
+
+
+@router.post("/payment/webhook")
+async def handle_yookassa_webhook(request: Request):
+ try:
+ data = await request.json()
+ event_type = data.get("event")
+
+ print(f"Webhook received: {event_type}")
+ try:
+ from tg_bot.config.settings import settings
+ from tg_bot.domain.services.user_service import UserService
+ from tg_bot.infrastructure.database.database import SessionLocal
+ from tg_bot.infrastructure.database.models import UserModel
+ from aiogram import Bot
+
+ session = SessionLocal()
+ if event_type == "payment.succeeded":
+ payment = data.get("object", {})
+ user_id = payment.get("metadata", {}).get("user_id")
+
+ if user_id:
+ user_service = UserService(session)
+ success = await user_service.activate_premium(int(user_id))
+ if success:
+ print(f"Premium activated for user {user_id}")
+
+ user = session.query(UserModel).filter_by(
+ telegram_id=str(user_id)
+ ).first()
+
+ if user and settings.TELEGRAM_BOT_TOKEN:
+ try:
+ bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
+ premium_until = user.premium_until or datetime.now() + timedelta(days=30)
+
+ notification = (
+ f"Оплата подтверждена!\n\n"
+ f"Premium активирован до {premium_until.strftime('%d.%m.%Y')}"
+ )
+
+ await bot.send_message(
+ chat_id=int(user_id),
+ text=notification,
+ parse_mode="HTML"
+ )
+ print(f"Notification sent to user {user_id}")
+ await bot.session.close()
+ except Exception as e:
+ print(f"Error sending notification: {e}")
+ else:
+ print(f"User {user_id} not found")
+ session.close()
+
+ except ImportError as e:
+ print(f"Import error: {e}")
+ return JSONResponse({"status": "ok", "message": "Webhook processed"})
+
+ except Exception as e:
+ print(f"Error processing webhook: {e}")
+ raise HTTPException(status_code=500, detail="Internal server error")
diff --git a/tg_bot/payment/yookassa/__init__.py b/tg_bot/payment/yookassa/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tg_bot/payment/yookassa/client.py b/tg_bot/payment/yookassa/client.py
new file mode 100644
index 0000000..3e3a09c
--- /dev/null
+++ b/tg_bot/payment/yookassa/client.py
@@ -0,0 +1,55 @@
+from decimal import Decimal
+import uuid
+from typing import Dict, Any
+from yookassa import Configuration, Payment as YooPayment
+from tg_bot.config.settings import settings
+
+
+class YookassaClient:
+
+ def __init__(self):
+ Configuration.configure(
+ account_id=settings.YOOKASSA_SHOP_ID,
+ secret_key=settings.YOOKASSA_SECRET_KEY
+ )
+
+ async def create_payment(
+ self,
+ amount: Decimal,
+ description: str,
+ user_id: int
+ ) -> Dict[str, Any]:
+ try:
+ payment = YooPayment.create({
+ "amount": {
+ "value": f"{amount:.2f}",
+ "currency": "RUB"
+ },
+ "payment_method_data": {
+ "type": "bank_card"
+ },
+ "confirmation": {
+ "type": "redirect",
+ "return_url": settings.YOOKASSA_RETURN_URL
+ },
+ "capture": True,
+ "description": description,
+ "metadata": {
+ "user_id": str(user_id),
+ "telegram_payment": "true"
+ },
+ "save_payment_method": False
+ })
+ return {
+ "id": payment.id,
+ "status": payment.status,
+ "confirmation_url": payment.confirmation.confirmation_url,
+ "amount": payment.amount.value,
+ "description": payment.description,
+ "metadata": payment.metadata
+ }
+ except Exception as e:
+ print(f"Error creating payment: {e}")
+ raise
+
+yookassa_client = YookassaClient()