change payments
This commit is contained in:
@@ -90,3 +90,6 @@ PAY = "Пополнить"
|
||||
PAY_2 = "Оплатить"
|
||||
CUSTOM_AMOUNT = "💰 Ввести свою сумму"
|
||||
STARS_BOT = "🤖 Бот для покупки звезд"
|
||||
WATA_RU = "🇷🇺 WATA: Карты РФ"
|
||||
WATA_SBP = "🏦 WATA: СБП"
|
||||
WATA_INT = "🌍 WATA: Международные карты"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -52,4 +52,6 @@ psycopg2-binary
|
||||
psutil
|
||||
uvicorn
|
||||
fastapi
|
||||
python-dateutil
|
||||
python-dateutil
|
||||
aiohttp>=3.8.0
|
||||
cryptography>=41.0.0
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user