From 6a0f4f8509d151bb188e9d853964cad06365f2b5 Mon Sep 17 00:00:00 2001 From: Max Shalenkov Date: Wed, 16 Jul 2025 19:49:49 +0300 Subject: [PATCH 1/4] change payments --- handlers/buttons.py | 3 + handlers/pay.py | 28 +++++ handlers/payments/wata.py | 234 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +- web/__init__.py | 4 + web/wata_payment.py | 66 +++++++++++ 6 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 handlers/payments/wata.py create mode 100644 web/wata_payment.py diff --git a/handlers/buttons.py b/handlers/buttons.py index 25e047e7..744d3c57 100644 --- a/handlers/buttons.py +++ b/handlers/buttons.py @@ -90,3 +90,6 @@ PAY = "Пополнить" PAY_2 = "Оплатить" CUSTOM_AMOUNT = "💰 Ввести свою сумму" STARS_BOT = "🤖 Бот для покупки звезд" +WATA_RU = "🇷🇺 WATA: Карты РФ" +WATA_SBP = "🏦 WATA: СБП" +WATA_INT = "🌍 WATA: Международные карты" diff --git a/handlers/pay.py b/handlers/pay.py index d1be4d12..d4cf40b2 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -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") diff --git a/handlers/payments/wata.py b/handlers/payments/wata.py new file mode 100644 index 00000000..e8ae0918 --- /dev/null +++ b/handlers/payments/wata.py @@ -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 diff --git a/requirements.txt b/requirements.txt index 04db0530..640f0fbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,4 +52,6 @@ psycopg2-binary psutil uvicorn fastapi -python-dateutil \ No newline at end of file +python-dateutil +aiohttp>=3.8.0 +cryptography>=41.0.0 \ No newline at end of file diff --git a/web/__init__.py b/web/__init__.py index e56973fd..135413cc 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -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) + diff --git a/web/wata_payment.py b/web/wata_payment.py new file mode 100644 index 00000000..2a3b3fcb --- /dev/null +++ b/web/wata_payment.py @@ -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) From e2ad136dd740d2c62b150b17df4442be66e34864 Mon Sep 17 00:00:00 2001 From: Max Shalenkov Date: Wed, 16 Jul 2025 23:59:33 +0300 Subject: [PATCH 2/4] fix wata --- handlers/payments/wata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/payments/wata.py b/handlers/payments/wata.py index e8ae0918..133716a3 100644 --- a/handlers/payments/wata.py +++ b/handlers/payments/wata.py @@ -12,7 +12,7 @@ from config import ( WATA_RU_ENABLE, WATA_RU_TOKEN, WATA_SBP_ENABLE, WATA_SBP_TOKEN, WATA_INT_ENABLE, WATA_INT_TOKEN, - SUCCESS_REDIRECT_LINK, + REDIRECT_LINK, FAIL_REDIRECT_LINK, ) from database import ( @@ -197,7 +197,7 @@ async def generate_wata_payment_link(amount, tg_id, cassa): "currency": "RUB", "orderId": str(tg_id), "orderDescription": WATA_PAYMENT_TITLE, - "successUrl": f"{SUCCESS_REDIRECT_LINK}", + "successUrl": f"{REDIRECT_LINK}", "failUrl": f"{FAIL_REDIRECT_LINK}", } #if cassa["name"] == "sbp": From 01e3a8f4560c29ece96a1f6259c20c4bcdc0b626 Mon Sep 17 00:00:00 2001 From: Max Shalenkov Date: Wed, 16 Jul 2025 22:22:40 +0000 Subject: [PATCH 3/4] final fix wata.pro --- handlers/payments/__init__.py | 2 + handlers/payments/wata.py | 114 ++++++++++++++++++++++++++++------ web/wata_payment.py | 32 ++++++---- 3 files changed, 117 insertions(+), 31 deletions(-) diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index ce2aefd4..0848fce9 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -18,6 +18,7 @@ from .robokassa_pay import router as robokassa_router from .stars_pay import router as stars_router from .yookassa_pay import router as yookassa_router from .yoomoney_pay import router as yoomoney_router +from .wata import router as wata_router router = Router(name="payments_main_router") @@ -34,4 +35,5 @@ if CRYPTO_BOT_ENABLE: if STARS_ENABLE: router.include_router(stars_router) +router.include_router(wata_router) router.include_router(gift_router) diff --git a/handlers/payments/wata.py b/handlers/payments/wata.py index 133716a3..b2833b64 100644 --- a/handlers/payments/wata.py +++ b/handlers/payments/wata.py @@ -39,6 +39,7 @@ class ReplenishBalanceWataState(StatesGroup): choosing_cassa = State() choosing_amount = State() waiting_for_payment_confirmation = State() + entering_custom_amount = State() # новое состояние для ввода суммы WATA_CASSA_CONFIG = [ {"enable": WATA_RU_ENABLE, "token": WATA_RU_TOKEN, "name": "ru", "button": WATA_RU, "desc": WATA_RU_DESCRIPTION}, @@ -51,7 +52,6 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: 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( @@ -81,13 +81,15 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}', ) ) + # Кнопка для ввода произвольной суммы + builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"wata_custom_amount|{cassa_name}")) 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.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id, wata_cassa=cassa_name) await state.set_state(ReplenishBalanceWataState.choosing_amount) return # Если cassa_name не передан — обычное меню выбора кассы @@ -146,9 +148,77 @@ async def process_cassa_selection(callback_query: types.CallbackQuery, state: FS 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_custom_amount|")) +async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext): + cassa_name = callback_query.data.split("|")[1] + await state.update_data(wata_cassa=cassa_name) + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_wata_{cassa_name}")) + await edit_or_send_message( + target_message=callback_query.message, + text=ENTER_SUM, + reply_markup=builder.as_markup(), + force_text=True, + ) + await state.set_state(ReplenishBalanceWataState.entering_custom_amount) + +# Обработчик ввода суммы пользователем +@router.message(ReplenishBalanceWataState.entering_custom_amount) +async def handle_custom_amount_input(message: types.Message, state: FSMContext): + data = await state.get_data() + cassa_name = data.get("wata_cassa") + 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=message, + text="Ошибка: выбранная касса недоступна.", + reply_markup=types.InlineKeyboardMarkup(), + force_text=True, + ) + return + try: + amount = int(message.text.strip()) + if amount <= 0: + raise ValueError + # Проверка для SBP: минимум 50 рублей + if cassa_name == "sbp" and amount < 50: + await edit_or_send_message( + target_message=message, + text="Минимальная сумма для оплаты через СБП — 50 рублей.", + reply_markup=types.InlineKeyboardMarkup(), + force_text=True, + ) + return + except Exception: + await edit_or_send_message( + target_message=message, + text="Некорректная сумма. Введите целое число больше 0.", + reply_markup=types.InlineKeyboardMarkup(), + force_text=True, + ) + return + await state.update_data(amount=amount) + payment_url = await generate_wata_payment_link(amount, 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=message, + text=WATA_PAYMENT_MESSAGE.format(amount=amount), + reply_markup=confirm_keyboard, + force_text=True, + ) + await state.set_state(ReplenishBalanceWataState.waiting_for_payment_confirmation) + @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("|") + parts = callback_query.data.split("|") + cassa_name = parts[1] + amount_str = parts[-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( @@ -187,7 +257,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F 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" + url = "https://api.wata.pro/api/h2h/links" headers = { "Authorization": f"Bearer {cassa['token']}", "Content-Type": "application/json", @@ -201,34 +271,42 @@ async def generate_wata_payment_link(amount, tg_id, cassa): "failUrl": f"{FAIL_REDIRECT_LINK}", } #if cassa["name"] == "sbp": - #data["paymentMethod"] = "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" # или другую валюту, если нужно + 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"] + if resp.status == 200: + try: + resp_json = await resp.json() + except Exception: + text = await resp.text() + logger.error(f"Ошибка при разборе JSON ответа WATA: статус={resp.status}, ответ={text}") + return "https://wata.pro/" + if "url" in resp_json: + return resp_json["url"] + else: + logger.error(f"Ответ WATA без url: {resp_json}") + return "https://wata.pro/" else: - logger.error(f"Ошибка при создании ссылки WATA: {resp_json}") - return "https://wata.pro/" # fallback + # Ошибка: логируем тело ответа + try: + error_json = await resp.json() + logger.error(f"Ошибка WATA API: статус={resp.status}, ответ={error_json}") + except Exception: + text = await resp.text() + logger.error(f"Ошибка WATA API: статус={resp.status}, не-JSON ответ: {text}") + return "https://wata.pro/" diff --git a/web/wata_payment.py b/web/wata_payment.py index 2a3b3fcb..cca7c599 100644 --- a/web/wata_payment.py +++ b/web/wata_payment.py @@ -46,20 +46,26 @@ async def wata_payment_webhook(request: web.Request): if not await verify_signature(raw_json, signature, public_key_pem): logger.error("Подпись WATA не прошла проверку!") return web.Response(status=400) + # Логируем всю транзакцию для аудита + logger.info(f"WATA webhook: {json.dumps(data, ensure_ascii=False)}") + # Логируем ключевые поля + logger.info(f"transactionId={data.get('transactionId')}, status={data.get('transactionStatus')}, orderId={data.get('orderId')}, amount={data.get('amount')}, currency={data.get('currency')}, errorCode={data.get('errorCode')}, errorDescription={data.get('errorDescription')}") # Проверяем статус оплаты - 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}") + if data.get("transactionStatus") == "Paid": + 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}") + elif data.get("transactionStatus") == "Declined": + logger.warning(f"WATA: транзакция отклонена: {data}") + else: + logger.warning(f"WATA: неизвестный статус транзакции: {data.get('transactionStatus')}") return web.Response(status=200) except Exception as e: logger.error(f"Ошибка в webhook WATA: {e}") From a8bc6209890e6f6956c60ea1c717c5549bc967a4 Mon Sep 17 00:00:00 2001 From: Max Shalenkov Date: Wed, 16 Jul 2025 22:49:58 +0000 Subject: [PATCH 4/4] remove comments --- handlers/pay.py | 1 - handlers/payments/wata.py | 12 ++++-------- web/wata_payment.py | 4 +--- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/handlers/pay.py b/handlers/pay.py index d4cf40b2..c1cd642e 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -70,7 +70,6 @@ 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) diff --git a/handlers/payments/wata.py b/handlers/payments/wata.py index b2833b64..248e2245 100644 --- a/handlers/payments/wata.py +++ b/handlers/payments/wata.py @@ -39,7 +39,7 @@ class ReplenishBalanceWataState(StatesGroup): choosing_cassa = State() choosing_amount = State() waiting_for_payment_confirmation = State() - entering_custom_amount = State() # новое состояние для ввода суммы + entering_custom_amount = State() WATA_CASSA_CONFIG = [ {"enable": WATA_RU_ENABLE, "token": WATA_RU_TOKEN, "name": "ru", "button": WATA_RU, "desc": WATA_RU_DESCRIPTION}, @@ -81,7 +81,6 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}', ) ) - # Кнопка для ввода произвольной суммы builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"wata_custom_amount|{cassa_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data="pay")) await callback_query.message.delete() @@ -92,7 +91,6 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id, wata_cassa=cassa_name) await state.set_state(ReplenishBalanceWataState.choosing_amount) return - # Если cassa_name не передан — обычное меню выбора кассы builder = InlineKeyboardBuilder() for cassa in WATA_CASSA_CONFIG: if cassa["enable"]: @@ -148,7 +146,6 @@ async def process_cassa_selection(callback_query: types.CallbackQuery, state: FS 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_custom_amount|")) async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext): cassa_name = callback_query.data.split("|")[1] @@ -163,7 +160,7 @@ async def process_custom_amount_button(callback_query: types.CallbackQuery, stat ) await state.set_state(ReplenishBalanceWataState.entering_custom_amount) -# Обработчик ввода суммы пользователем + @router.message(ReplenishBalanceWataState.entering_custom_amount) async def handle_custom_amount_input(message: types.Message, state: FSMContext): data = await state.get_data() @@ -270,8 +267,7 @@ async def generate_wata_payment_link(amount, tg_id, cassa): "successUrl": f"{REDIRECT_LINK}", "failUrl": f"{FAIL_REDIRECT_LINK}", } - #if cassa["name"] == "sbp": - # data["paymentMethod"] = "SBP" + if cassa["name"] == "int": import json async def get_usd_rate(): @@ -302,7 +298,7 @@ async def generate_wata_payment_link(amount, tg_id, cassa): logger.error(f"Ответ WATA без url: {resp_json}") return "https://wata.pro/" else: - # Ошибка: логируем тело ответа + try: error_json = await resp.json() logger.error(f"Ошибка WATA API: статус={resp.status}, ответ={error_json}") diff --git a/web/wata_payment.py b/web/wata_payment.py index cca7c599..875ce775 100644 --- a/web/wata_payment.py +++ b/web/wata_payment.py @@ -46,11 +46,9 @@ async def wata_payment_webhook(request: web.Request): if not await verify_signature(raw_json, signature, public_key_pem): logger.error("Подпись WATA не прошла проверку!") return web.Response(status=400) - # Логируем всю транзакцию для аудита + logger.info(f"WATA webhook: {json.dumps(data, ensure_ascii=False)}") - # Логируем ключевые поля logger.info(f"transactionId={data.get('transactionId')}, status={data.get('transactionStatus')}, orderId={data.get('orderId')}, amount={data.get('amount')}, currency={data.get('currency')}, errorCode={data.get('errorCode')}, errorDescription={data.get('errorDescription')}") - # Проверяем статус оплаты if data.get("transactionStatus") == "Paid": tg_id = data.get("orderId") amount = data.get("amount")