change payments

This commit is contained in:
Max Shalenkov
2025-07-16 19:49:49 +03:00
parent 52aa6f81e2
commit 9ee2f0addf
6 changed files with 338 additions and 1 deletions
+3
View File
@@ -90,3 +90,6 @@ PAY = "Пополнить"
PAY_2 = "Оплатить"
CUSTOM_AMOUNT = "💰 Ввести свою сумму"
STARS_BOT = "🤖 Бот для покупки звезд"
WATA_RU = "🇷🇺 WATA: Карты РФ"
WATA_SBP = "🏦 WATA: СБП"
WATA_INT = "🌍 WATA: Международные карты"
+28
View File
@@ -16,6 +16,9 @@ from config import (
STARS_ENABLE,
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
WATA_RU_ENABLE,
WATA_SBP_ENABLE,
WATA_INT_ENABLE,
)
from database import get_last_payments
from database.models import User
@@ -30,6 +33,9 @@ from handlers.buttons import (
STARS,
YOOKASSA,
YOOMONEY,
WATA_RU,
WATA_SBP,
WATA_INT,
)
from handlers.payments.cryprobot_pay import process_callback_pay_cryptobot
from handlers.payments.freekassa_pay import process_callback_pay_freekassa
@@ -37,6 +43,7 @@ from handlers.payments.robokassa_pay import process_callback_pay_robokassa
from handlers.payments.stars_pay import process_callback_pay_stars
from handlers.payments.yookassa_pay import process_callback_pay_yookassa
from handlers.payments.yoomoney_pay import process_callback_pay_yoomoney
from handlers.payments.wata import process_callback_pay_wata
from handlers.texts import BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG
from .utils import edit_or_send_message
@@ -63,6 +70,9 @@ async def handle_pay(
payment_handlers.append(process_callback_pay_robokassa)
if FREEKASSA_ENABLE:
payment_handlers.append(process_callback_pay_freekassa)
# WATA
if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE:
payment_handlers.append(process_callback_pay_wata)
if len(payment_handlers) == 1:
await callback_query.answer()
@@ -82,6 +92,12 @@ async def handle_pay(
builder.row(InlineKeyboardButton(text=ROBOKASSA, callback_data="pay_robokassa"))
if FREEKASSA_ENABLE:
builder.row(InlineKeyboardButton(text=FREEKASSA, callback_data="pay_freekassa"))
if WATA_RU_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_RU, callback_data="pay_wata_ru"))
if WATA_SBP_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_SBP, callback_data="pay_wata_sbp"))
if WATA_INT_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_INT, callback_data="pay_wata_int"))
if DONATIONS_ENABLE:
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
@@ -150,3 +166,15 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any):
media_path=None,
disable_web_page_preview=False,
)
@router.callback_query(F.data == "pay_wata_ru")
async def handle_pay_wata_ru(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="ru")
@router.callback_query(F.data == "pay_wata_sbp")
async def handle_pay_wata_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="sbp")
@router.callback_query(F.data == "pay_wata_int")
async def handle_pay_wata_int(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="int")
+234
View File
@@ -0,0 +1,234 @@
import aiohttp
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime, timedelta
from sqlalchemy import select, and_
from config import (
WATA_RU_ENABLE, WATA_RU_TOKEN,
WATA_SBP_ENABLE, WATA_SBP_TOKEN,
WATA_INT_ENABLE, WATA_INT_TOKEN,
SUCCESS_REDIRECT_LINK,
FAIL_REDIRECT_LINK,
)
from database import (
add_payment,
add_user,
async_session_maker,
check_user_exists,
get_key_count,
get_temporary_data,
update_balance,
Payment
)
from handlers.buttons import BACK, PAY_2, WATA_RU, WATA_SBP, WATA_INT
from handlers.texts import (
WATA_RU_DESCRIPTION, WATA_SBP_DESCRIPTION, WATA_INT_DESCRIPTION,
WATA_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS, WATA_PAYMENT_TITLE
)
from handlers.utils import edit_or_send_message
from logger import logger
router = Router()
class ReplenishBalanceWataState(StatesGroup):
choosing_cassa = State()
choosing_amount = State()
waiting_for_payment_confirmation = State()
WATA_CASSA_CONFIG = [
{"enable": WATA_RU_ENABLE, "token": WATA_RU_TOKEN, "name": "ru", "button": WATA_RU, "desc": WATA_RU_DESCRIPTION},
{"enable": WATA_SBP_ENABLE, "token": WATA_SBP_TOKEN, "name": "sbp", "button": WATA_SBP, "desc": WATA_SBP_DESCRIPTION},
{"enable": WATA_INT_ENABLE, "token": WATA_INT_TOKEN, "name": "int", "button": WATA_INT, "desc": WATA_INT_DESCRIPTION},
]
@router.callback_query(F.data == "pay_wata")
async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, cassa_name: str = None):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated WATA payment.")
if cassa_name:
# Сразу открываем выбор суммы для нужной кассы
cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name and c["enable"]), None)
if not cassa:
await edit_or_send_message(
target_message=callback_query.message,
text="Ошибка: выбранная касса недоступна.",
reply_markup=types.InlineKeyboardMarkup(),
force_text=True,
)
return
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay"))
await callback_query.message.delete()
new_message = await callback_query.message.answer(
text=cassa["desc"],
reply_markup=builder.as_markup(),
)
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id)
await state.set_state(ReplenishBalanceWataState.choosing_amount)
return
# Если cassa_name не передан — обычное меню выбора кассы
builder = InlineKeyboardBuilder()
for cassa in WATA_CASSA_CONFIG:
if cassa["enable"]:
builder.row(InlineKeyboardButton(text=cassa["button"], callback_data=f'wata_cassa|{cassa["name"]}'))
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
await callback_query.message.delete()
new_message = await callback_query.message.answer(
text="Выберите способ оплаты через WATA:",
reply_markup=builder.as_markup(),
)
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id)
await state.set_state(ReplenishBalanceWataState.choosing_cassa)
@router.callback_query(F.data.startswith("wata_cassa|"))
async def process_cassa_selection(callback_query: types.CallbackQuery, state: FSMContext):
cassa_name = callback_query.data.split("|")[1]
cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name), None)
if not cassa or not cassa["enable"]:
await edit_or_send_message(
target_message=callback_query.message,
text="Ошибка: выбранная касса недоступна.",
reply_markup=types.InlineKeyboardMarkup(),
force_text=True,
)
return
await state.update_data(wata_cassa=cassa_name)
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_wata"))
await callback_query.message.delete()
new_message = await callback_query.message.answer(
text=cassa["desc"],
reply_markup=builder.as_markup(),
)
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id)
await state.set_state(ReplenishBalanceWataState.choosing_amount)
@router.callback_query(F.data.startswith("wata_amount|"))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
_, cassa_name, amount_str = callback_query.data.split("|")
cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name), None)
if not cassa or not cassa["enable"]:
await edit_or_send_message(
target_message=callback_query.message,
text="Ошибка: выбранная касса недоступна.",
reply_markup=types.InlineKeyboardMarkup(),
force_text=True,
)
return
try:
amount = int(amount_str)
if amount <= 0:
raise ValueError
except Exception:
await edit_or_send_message(
target_message=callback_query.message,
text="Некорректная сумма.",
reply_markup=types.InlineKeyboardMarkup(),
force_text=True,
)
return
await state.update_data(amount=amount)
payment_url = await generate_wata_payment_link(amount, callback_query.message.chat.id, cassa)
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="pay_wata")],
]
)
await edit_or_send_message(
target_message=callback_query.message,
text=WATA_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard,
force_text=True,
)
await state.set_state(ReplenishBalanceWataState.waiting_for_payment_confirmation)
async def generate_wata_payment_link(amount, tg_id, cassa):
url = "https://api.wata.pro/api/h2h/payment-link"
headers = {
"Authorization": f"Bearer {cassa['token']}",
"Content-Type": "application/json",
}
data = {
"amount": float(amount),
"currency": "RUB",
"orderId": str(tg_id),
"orderDescription": WATA_PAYMENT_TITLE,
"successUrl": f"{SUCCESS_REDIRECT_LINK}",
"failUrl": f"{FAIL_REDIRECT_LINK}",
}
#if cassa["name"] == "sbp":
#data["paymentMethod"] = "SBP"
if cassa["name"] == "int":
# Здесь конвертируем сумму из рублей в USD по курсу гугла + 5 рублей к курсу
import json
async def get_usd_rate():
# Получаем курс доллара с помощью публичного API (например, exchangerate.host)
url = "https://api.exchangerate.host/latest?base=RUB&symbols=USD"
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=10) as resp:
data = await resp.json()
return data["rates"]["USD"]
usd_rate = await get_usd_rate()
# Прибавляем 5 рублей к курсу (то есть к 1 USD в рублях)
# 1 USD = 1 / usd_rate RUB, значит RUB за 1 USD = 1 / usd_rate
rub_per_usd = 1 / usd_rate
rub_per_usd_plus_5 = rub_per_usd + 5
# Новый курс USD = 1 / (RUB за 1 USD + 5)
new_usd_rate = 1 / rub_per_usd_plus_5
amount_usd = round(float(amount) * new_usd_rate, 2)
data["amount"] = amount_usd
data["currency"] = "USD" # или другую валюту, если нужно
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=data, timeout=60) as resp:
resp_json = await resp.json()
if resp.status == 200 and "paymentLink" in resp_json:
return resp_json["paymentLink"]
else:
logger.error(f"Ошибка при создании ссылки WATA: {resp_json}")
return "https://wata.pro/" # fallback
+3 -1
View File
@@ -52,4 +52,6 @@ psycopg2-binary
psutil
uvicorn
fastapi
python-dateutil
python-dateutil
aiohttp>=3.8.0
cryptography>=41.0.0
+4
View File
@@ -4,7 +4,11 @@ import bot
from config import TBLOCKER_WEBHOOK_PATH
from .tblocker import tblocker_webhook
from .wata_payment import wata_payment_webhook
WATA_WEBHOOK_PATH = "/wata/webhook"
async def register_web_routes(router: UrlDispatcher) -> None:
router.add_post(TBLOCKER_WEBHOOK_PATH, tblocker_webhook)
router.add_post(WATA_WEBHOOK_PATH, wata_payment_webhook)
+66
View File
@@ -0,0 +1,66 @@
import base64
import aiohttp
from aiohttp import web
import json
import logging
from database import async_session_maker, update_balance, add_payment
from handlers.payments.utils import send_payment_success_notification
from config import WATA_RU_TOKEN, WATA_SBP_TOKEN, WATA_INT_TOKEN
from logger import logger
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
PUBLIC_KEY_URL = "https://api.wata.pro/api/h2h/public-key"
async def get_wata_public_key():
async with aiohttp.ClientSession() as session:
async with session.get(PUBLIC_KEY_URL) as resp:
data = await resp.json()
return data["value"].encode()
async def verify_signature(raw_json: bytes, signature: str, public_key_pem: bytes) -> bool:
try:
public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())
signature_bytes = base64.b64decode(signature)
public_key.verify(
signature_bytes,
raw_json,
padding.PKCS1v15(),
hashes.SHA512()
)
return True
except Exception as e:
logger.error(f"Ошибка проверки подписи WATA: {e}")
return False
async def wata_payment_webhook(request: web.Request):
try:
raw_json = await request.read()
data = json.loads(raw_json)
signature = request.headers.get("X-Signature")
if not signature:
logger.error("Нет подписи X-Signature в заголовке!")
return web.Response(status=400)
public_key_pem = await get_wata_public_key()
if not await verify_signature(raw_json, signature, public_key_pem):
logger.error("Подпись WATA не прошла проверку!")
return web.Response(status=400)
# Проверяем статус оплаты
if data.get("transactionStatus") != "Paid":
logger.warning(f"WATA: транзакция не оплачена: {data}")
return web.Response(status=200)
tg_id = data.get("orderId")
amount = data.get("amount")
if not tg_id or not amount:
logger.error(f"WATA: отсутствует orderId или amount: {data}")
return web.Response(status=400)
async with async_session_maker() as session:
await update_balance(session, int(tg_id), float(amount))
await send_payment_success_notification(tg_id, float(amount), session)
await add_payment(session, int(tg_id), float(amount), "wata")
logger.info(f"✅ WATA: баланс пополнен для пользователя {tg_id} на {amount}")
return web.Response(status=200)
except Exception as e:
logger.error(f"Ошибка в webhook WATA: {e}")
return web.Response(status=500)