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()