polina_tg #1
@ -8,7 +8,8 @@ from tg_bot.infrastructure.telegram.handlers import (
|
|||||||
start_handler,
|
start_handler,
|
||||||
help_handler,
|
help_handler,
|
||||||
stats_handler,
|
stats_handler,
|
||||||
question_handler
|
question_handler,
|
||||||
|
buy_handler
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -24,6 +25,7 @@ async def create_bot() -> tuple[Bot, Dispatcher]:
|
|||||||
dp.include_router(help_handler.router)
|
dp.include_router(help_handler.router)
|
||||||
dp.include_router(stats_handler.router)
|
dp.include_router(stats_handler.router)
|
||||||
dp.include_router(question_handler.router)
|
dp.include_router(question_handler.router)
|
||||||
|
dp.include_router(buy_handler.router)
|
||||||
return bot, dp
|
return bot, dp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
283
tg_bot/infrastructure/telegram/handlers/buy_handler.py
Normal file
283
tg_bot/infrastructure/telegram/handlers/buy_handler.py
Normal file
@ -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"<b>У вас уже есть активная подписка!</b>\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"<b>Оплата подписки VibeLawyerBot</b>\n\n"
|
||||||
|
f"<b>Детали платежа:</b>\n"
|
||||||
|
f"• Сумма: {settings.PAYMENT_AMOUNT} руб.\n"
|
||||||
|
f"• Описание: Подписка на 30 дней\n"
|
||||||
|
f"• ID платежа: <code>{payment_data['id'][:20]}...</code>\n\n"
|
||||||
|
f"<b>Что даёт подписка:</b>\n"
|
||||||
|
f"• Неограниченное число вопросов\n"
|
||||||
|
f"• Приоритетная обработка\n"
|
||||||
|
f"• Доступ ко всем функциям\n\n"
|
||||||
|
f"<i>После оплаты доступ активируется автоматически в течение 1-2 минут.</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
response_text,
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=keyboard,
|
||||||
|
disable_web_page_preview=True
|
||||||
|
)
|
||||||
|
await message.answer(
|
||||||
|
"<b>Инструкция по оплате:</b>\n\n"
|
||||||
|
"1. Нажмите кнопку 'Оплатить онлайн'\n"
|
||||||
|
"2. Введите данные банковской карты\n"
|
||||||
|
"3. Подтвердите оплату\n"
|
||||||
|
"4. После успешной оплаты нажмите 'Проверить статус оплаты'\n\n"
|
||||||
|
"<b>Тестовые карты для проверки:</b>\n"
|
||||||
|
"• <code>5555 5555 5555 4477</code> - успешная оплата\n"
|
||||||
|
" Срок: <b>любой будущий</b> (напр. 12/30)\n"
|
||||||
|
" CVV: <b>любые 3 цифры</b> (напр. 123)\n\n"
|
||||||
|
"<i>Это тестовые карты, реальные деньги не списываются!</i>",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка создания платежа: {e}")
|
||||||
|
await message.answer(
|
||||||
|
"<b>Произошла ошибка при создании платежа</b>\n\n"
|
||||||
|
"Пожалуйста, попробуйте позже или обратитесь к администратору.\n\n"
|
||||||
|
f"<code>Ошибка: {str(e)[:100]}</code>",
|
||||||
|
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(
|
||||||
|
"<b>Оплата подтверждена!</b>\n\n"
|
||||||
|
f"Ваш premium-доступ активирован до: "
|
||||||
|
f"<b>{user.premium_until.strftime('%d.%m.%Y')}</b>\n\n"
|
||||||
|
"Теперь вы можете:\n"
|
||||||
|
"• Задавать неограниченное количество вопросов\n"
|
||||||
|
"• Получать приоритетные ответы\n"
|
||||||
|
"• Использовать все функции бота\n\n"
|
||||||
|
"<i>Спасибо за покупку!</i>",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await callback_query.message.answer(
|
||||||
|
"<b>Платёж найден в ЮKассе, но не в нашей БД</b>\n\n"
|
||||||
|
"Пожалуйста, обратитесь к администратору.",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
elif payment.status == "pending":
|
||||||
|
await callback_query.message.answer(
|
||||||
|
"<b>Платёж ещё не завершён</b>\n\n"
|
||||||
|
"Если вы уже оплатили, пожалуйста, подождите 1-2 минуты "
|
||||||
|
"и проверьте статус снова.\n\n"
|
||||||
|
"<i>Проверьте правильность данных карты:</i>\n"
|
||||||
|
"• Срок действия должен быть <b>будущим</b>\n"
|
||||||
|
"• CVV - <b>3 цифры</b> на обратной стороне карты",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await callback_query.message.answer(
|
||||||
|
f"<b>Статус платежа: {payment.status}</b>\n\n"
|
||||||
|
"Попробуйте оплатить ещё раз или обратитесь в поддержку.\n\n"
|
||||||
|
"<i>Для теста используйте карту:</i>\n"
|
||||||
|
"<code>5555 5555 5555 4477</code>\n"
|
||||||
|
"Срок: <b>12/30</b>, CVV: <b>123</b>",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка проверки статуса: {e}")
|
||||||
|
await callback_query.message.answer(
|
||||||
|
"<b>Не удалось проверить статус платежа</b>\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(
|
||||||
|
"<b>У вас пока нет платежей</b>\n\n"
|
||||||
|
"Используйте команду /buy чтобы оформить подписку.",
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
response = ["<b>Ваши последние платежи:</b>\n"]
|
||||||
|
|
||||||
|
for i, payment in enumerate(payments, 1):
|
||||||
|
status_text = "Успешно" if payment.status == "succeeded" else "Ожидание" if payment.status == "pending" else "Ошибка"
|
||||||
|
response.append(
|
||||||
|
f"\n<b>{i}. {payment.amount} руб. ({status_text})</b>\n"
|
||||||
|
f"Статус: {payment.status}\n"
|
||||||
|
f"Дата: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||||
|
f"ID: <code>{payment.payment_id[:8]}...</code>"
|
||||||
|
)
|
||||||
|
|
||||||
|
response.append("\n\n<i>Полный доступ открывается после успешной оплаты</i>")
|
||||||
|
|
||||||
|
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"<b>Тестовые банковские карты для оплаты</b>\n\n"
|
||||||
|
|
||||||
|
f"<b>Для тестирования оплаты используйте:</b>\n\n"
|
||||||
|
|
||||||
|
f"<b>Карта для успешной оплаты:</b>\n"
|
||||||
|
f"• Номер: <code>5555 5555 5555 4477</code>\n"
|
||||||
|
f"• Срок действия: <b>ЛЮБОЙ будущий</b> (например: 12/30)\n"
|
||||||
|
f"• CVV код: <b>ЛЮБЫЕ 3 цифры</b> (например: 123)\n"
|
||||||
|
f"• Результат: Оплата пройдёт успешно\n\n"
|
||||||
|
|
||||||
|
f"<b>Карта для отказа в оплате:</b>\n"
|
||||||
|
f"• Номер: <code>5555 5555 5555 4445</code>\n"
|
||||||
|
f"• Срок действия: <b>ЛЮБОЙ будущий</b>\n"
|
||||||
|
f"• CVV код: <b>ЛЮБЫЕ 3 цифры</b>\n"
|
||||||
|
f"• Результат: Оплата будет отклонена\n\n"
|
||||||
|
|
||||||
|
f"<b>Важно:</b>\n"
|
||||||
|
f"• Это тестовые карты, реальные деньги не списываются\n"
|
||||||
|
f"• Используются только для проверки работы оплаты\n"
|
||||||
|
f"• После успешной тестовой оплаты premium активируется\n\n"
|
||||||
|
|
||||||
|
f"Для оплаты подписки используйте команду /buy"
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.answer(testcards_text, parse_mode="HTML")
|
||||||
0
tg_bot/payment/__init__.py
Normal file
0
tg_bot/payment/__init__.py
Normal file
0
tg_bot/payment/webhooks/__init__.py
Normal file
0
tg_bot/payment/webhooks/__init__.py
Normal file
70
tg_bot/payment/webhooks/handler.py
Normal file
70
tg_bot/payment/webhooks/handler.py
Normal file
@ -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"<b>Оплата подтверждена!</b>\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")
|
||||||
0
tg_bot/payment/yookassa/__init__.py
Normal file
0
tg_bot/payment/yookassa/__init__.py
Normal file
55
tg_bot/payment/yookassa/client.py
Normal file
55
tg_bot/payment/yookassa/client.py
Normal file
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user