@@ -94,4 +94,6 @@ STARS_BOT = "🤖 Бот для покупки звезд"
|
||||
WATA_RU = "🇷🇺 WATA: Карты РФ"
|
||||
WATA_SBP = "🏦 WATA: СБП"
|
||||
WATA_INT = "🌍 WATA: Международные карты"
|
||||
KASSAI_CARDS = "💳 KassaAI: Карты РФ"
|
||||
KASSAI_SBP = "🏦 KassaAI: СБП"
|
||||
STARS_BOT_URL = "https://t.me/PremiumBot"
|
||||
|
||||
+21
-2
@@ -13,6 +13,7 @@ from config import (
|
||||
CRYPTO_BOT_ENABLE,
|
||||
DONATIONS_ENABLE,
|
||||
FREEKASSA_ENABLE,
|
||||
KASSAI_ENABLE,
|
||||
ROBOKASSA_ENABLE,
|
||||
STARS_ENABLE,
|
||||
WATA_INT_ENABLE,
|
||||
@@ -28,6 +29,8 @@ from handlers.buttons import (
|
||||
COUPON,
|
||||
CRYPTOBOT,
|
||||
FREEKASSA,
|
||||
KASSAI_CARDS,
|
||||
KASSAI_SBP,
|
||||
MAIN_MENU,
|
||||
PAYMENT,
|
||||
ROBOKASSA,
|
||||
@@ -40,6 +43,7 @@ from handlers.buttons import (
|
||||
)
|
||||
from handlers.payments.cryprobot_pay import process_callback_pay_cryptobot
|
||||
from handlers.payments.freekassa_pay import process_callback_pay_freekassa
|
||||
from handlers.payments.kassai import process_callback_pay_kassai
|
||||
from handlers.payments.robokassa_pay import process_callback_pay_robokassa
|
||||
from handlers.payments.stars_pay import process_callback_pay_stars
|
||||
from handlers.payments.wata import process_callback_pay_wata
|
||||
@@ -61,6 +65,10 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
|
||||
payment_handlers.append(process_callback_pay_yookassa)
|
||||
if YOOMONEY_ENABLE:
|
||||
payment_handlers.append(process_callback_pay_yoomoney)
|
||||
if KASSAI_ENABLE:
|
||||
payment_handlers.append(process_callback_pay_kassai)
|
||||
if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE:
|
||||
payment_handlers.append(process_callback_pay_wata)
|
||||
if CRYPTO_BOT_ENABLE:
|
||||
payment_handlers.append(process_callback_pay_cryptobot)
|
||||
if STARS_ENABLE:
|
||||
@@ -69,8 +77,6 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
|
||||
payment_handlers.append(process_callback_pay_robokassa)
|
||||
if FREEKASSA_ENABLE:
|
||||
payment_handlers.append(process_callback_pay_freekassa)
|
||||
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 +88,9 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
|
||||
builder.row(InlineKeyboardButton(text=YOOKASSA, callback_data="pay_yookassa"))
|
||||
if YOOMONEY_ENABLE:
|
||||
builder.row(InlineKeyboardButton(text=YOOMONEY, callback_data="pay_yoomoney"))
|
||||
if KASSAI_ENABLE:
|
||||
builder.row(InlineKeyboardButton(text=KASSAI_CARDS, callback_data="pay_kassai_cards"))
|
||||
builder.row(InlineKeyboardButton(text=KASSAI_SBP, callback_data="pay_kassai_sbp"))
|
||||
if CRYPTO_BOT_ENABLE:
|
||||
builder.row(InlineKeyboardButton(text=CRYPTOBOT, callback_data="pay_cryptobot"))
|
||||
if STARS_ENABLE:
|
||||
@@ -175,3 +184,13 @@ async def handle_pay_wata_sbp(callback_query: CallbackQuery, state: FSMContext,
|
||||
@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")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pay_kassai_cards")
|
||||
async def handle_pay_kassai_cards(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
await process_callback_pay_kassai(callback_query, state, session, method_name="cards")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pay_kassai_sbp")
|
||||
async def handle_pay_kassai_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
await process_callback_pay_kassai(callback_query, state, session, method_name="sbp")
|
||||
|
||||
@@ -5,6 +5,7 @@ from aiogram import Router
|
||||
from config import (
|
||||
CRYPTO_BOT_ENABLE,
|
||||
FREEKASSA_ENABLE,
|
||||
KASSAI_ENABLE,
|
||||
ROBOKASSA_ENABLE,
|
||||
STARS_ENABLE,
|
||||
YOOKASSA_ENABLE,
|
||||
@@ -14,6 +15,7 @@ from config import (
|
||||
from .cryprobot_pay import router as cryprobot_router
|
||||
from .freekassa_pay import router as freekassa_router
|
||||
from .gift import router as gift_router
|
||||
from .kassai import router as kassai_router
|
||||
from .robokassa_pay import router as robokassa_router
|
||||
from .stars_pay import router as stars_router
|
||||
from .yookassa_pay import router as yookassa_router
|
||||
@@ -34,6 +36,8 @@ if CRYPTO_BOT_ENABLE:
|
||||
router.include_router(cryprobot_router)
|
||||
if STARS_ENABLE:
|
||||
router.include_router(stars_router)
|
||||
if KASSAI_ENABLE:
|
||||
router.include_router(kassai_router)
|
||||
|
||||
router.include_router(wata_router)
|
||||
router.include_router(gift_router)
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
import aiohttp
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
import json
|
||||
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 config import (
|
||||
KASSAI_ENABLE, KASSAI_API_KEY, KASSAI_SECRET_KEY, KASSAI_DOMAIN, KASSAI_SHOP_ID,
|
||||
REDIRECT_LINK, FAIL_REDIRECT_LINK, WEBHOOK_HOST, KASSAI_IP
|
||||
)
|
||||
|
||||
from handlers.buttons import BACK, PAY_2, KASSAI_CARDS, KASSAI_SBP
|
||||
from handlers.texts import (
|
||||
KASSAI_CARDS_DESCRIPTION, KASSAI_SBP_DESCRIPTION,
|
||||
KASSAI_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS, KASSAI_PAYMENT_TITLE
|
||||
)
|
||||
from handlers.utils import edit_or_send_message
|
||||
from logger import logger
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class ReplenishBalanceKassaiState(StatesGroup):
|
||||
choosing_method = State()
|
||||
choosing_amount = State()
|
||||
waiting_for_payment_confirmation = State()
|
||||
entering_custom_amount = State()
|
||||
|
||||
|
||||
KASSAI_PAYMENT_METHODS = [
|
||||
{"enable": KASSAI_ENABLE, "method": 36, "name": "cards", "button": KASSAI_CARDS, "desc": KASSAI_CARDS_DESCRIPTION},
|
||||
{"enable": KASSAI_ENABLE, "method": 44, "name": "sbp", "button": KASSAI_SBP, "desc": KASSAI_SBP_DESCRIPTION},
|
||||
]
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pay_kassai")
|
||||
async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, method_name: str = None):
|
||||
tg_id = callback_query.message.chat.id
|
||||
logger.info(f"User {tg_id} initiated KassaAI payment.")
|
||||
|
||||
if method_name:
|
||||
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None)
|
||||
if not method:
|
||||
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'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}',
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i + 1]["text"],
|
||||
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"].split("|")[1]}',
|
||||
),
|
||||
)
|
||||
else:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i]["text"],
|
||||
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}',
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}"))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay"))
|
||||
|
||||
await callback_query.message.delete()
|
||||
new_message = await callback_query.message.answer(
|
||||
text=method["desc"],
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id, kassai_method=method_name)
|
||||
await state.set_state(ReplenishBalanceKassaiState.choosing_amount)
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for method in KASSAI_PAYMENT_METHODS:
|
||||
if method["enable"]:
|
||||
builder.row(InlineKeyboardButton(text=method["button"], callback_data=f'kassai_method|{method["name"]}'))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
|
||||
|
||||
await callback_query.message.delete()
|
||||
new_message = await callback_query.message.answer(
|
||||
text="Выберите способ оплаты через KassaAI:",
|
||||
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(ReplenishBalanceKassaiState.choosing_method)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("kassai_method|"))
|
||||
async def process_method_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
method_name = callback_query.data.split("|")[1]
|
||||
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
|
||||
|
||||
if not method or not method["enable"]:
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="Ошибка: выбранный способ оплаты недоступен.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
force_text=True,
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(kassai_method=method_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'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}',
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i + 1]["text"],
|
||||
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"].split("|")[1]}',
|
||||
),
|
||||
)
|
||||
else:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i]["text"],
|
||||
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}',
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}"))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_kassai"))
|
||||
|
||||
await callback_query.message.delete()
|
||||
new_message = await callback_query.message.answer(
|
||||
text=method["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(ReplenishBalanceKassaiState.choosing_amount)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("kassai_custom_amount|"))
|
||||
async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
method_name = callback_query.data.split("|")[1]
|
||||
await state.update_data(kassai_method=method_name)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_kassai_{method_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(ReplenishBalanceKassaiState.entering_custom_amount)
|
||||
|
||||
|
||||
@router.message(ReplenishBalanceKassaiState.entering_custom_amount)
|
||||
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
|
||||
data = await state.get_data()
|
||||
method_name = data.get("kassai_method")
|
||||
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
|
||||
|
||||
if not method or not method["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
|
||||
if method_name == "sbp" and amount < 10:
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="Минимальная сумма для оплаты через СБП — 10 рублей.",
|
||||
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_kassai_payment_link(amount, message.chat.id, method)
|
||||
|
||||
confirm_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
|
||||
[InlineKeyboardButton(text=BACK, callback_data="pay_kassai")],
|
||||
]
|
||||
)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text=KASSAI_PAYMENT_MESSAGE.format(amount=amount),
|
||||
reply_markup=confirm_keyboard,
|
||||
force_text=True,
|
||||
)
|
||||
await state.set_state(ReplenishBalanceKassaiState.waiting_for_payment_confirmation)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("kassai_amount|"))
|
||||
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
parts = callback_query.data.split("|")
|
||||
method_name = parts[1]
|
||||
amount_str = parts[2]
|
||||
|
||||
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
|
||||
|
||||
if not method or not method["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_kassai_payment_link(amount, callback_query.message.chat.id, method)
|
||||
|
||||
confirm_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
|
||||
[InlineKeyboardButton(text=BACK, callback_data="pay_kassai")],
|
||||
]
|
||||
)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=KASSAI_PAYMENT_MESSAGE.format(amount=amount),
|
||||
reply_markup=confirm_keyboard,
|
||||
force_text=True,
|
||||
)
|
||||
await state.set_state(ReplenishBalanceKassaiState.waiting_for_payment_confirmation)
|
||||
|
||||
|
||||
async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) -> str:
|
||||
"""
|
||||
Создание заказа в KassaAI и получение ссылки на оплату
|
||||
"""
|
||||
nonce = int(time.time())
|
||||
unique_payment_id = f"{nonce}_{tg_id}"
|
||||
url = f"https://api.fk.life/v1/orders/create?paymentId={unique_payment_id}"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
client_email = f"{tg_id}@{KASSAI_DOMAIN}"
|
||||
client_ip = KASSAI_IP
|
||||
|
||||
data_for_signature = {
|
||||
"shopId": KASSAI_SHOP_ID,
|
||||
"nonce": nonce,
|
||||
"i": method["method"],
|
||||
"email": client_email,
|
||||
"ip": client_ip,
|
||||
"amount": int(amount),
|
||||
"currency": "RUB"
|
||||
}
|
||||
|
||||
sorted_keys = sorted(data_for_signature.keys())
|
||||
values = [str(data_for_signature[key]) for key in sorted_keys]
|
||||
sign_string = "|".join(values)
|
||||
|
||||
signature = hmac.new(
|
||||
KASSAI_API_KEY.encode('utf-8'),
|
||||
sign_string.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
data = data_for_signature.copy()
|
||||
data["signature"] = signature
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers, json=data, timeout=60) as resp:
|
||||
if resp.status == 200:
|
||||
try:
|
||||
resp_json = await resp.json()
|
||||
if resp_json.get("type") == "success":
|
||||
payment_url = resp_json.get("location")
|
||||
if payment_url:
|
||||
logger.info(f"KassaAI payment URL created for user {tg_id}")
|
||||
return payment_url
|
||||
else:
|
||||
logger.error(f"KassaAI: No location in response: {resp_json}")
|
||||
return "https://fk.life/"
|
||||
else:
|
||||
logger.error(f"KassaAI: Unsuccessful response: {resp_json}")
|
||||
return "https://fk.life/"
|
||||
except Exception as e:
|
||||
logger.error(f"KassaAI: Error parsing JSON response: {e}")
|
||||
text = await resp.text()
|
||||
logger.error(f"KassaAI: Response content: {text}")
|
||||
return "https://fk.life/"
|
||||
else:
|
||||
try:
|
||||
error_json = await resp.json()
|
||||
logger.error(f"KassaAI API error: status={resp.status}, response={error_json}")
|
||||
except Exception:
|
||||
text = await resp.text()
|
||||
logger.error(f"KassaAI API error: status={resp.status}, non-JSON response: {text}")
|
||||
return "https://fk.life/"
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating KassaAI order: {e}")
|
||||
return "https://fk.life/"
|
||||
|
||||
|
||||
def verify_kassai_signature(data: dict, signature: str) -> bool:
|
||||
"""
|
||||
Проверка подписи вебхука KassaAI согласно документации FreeKassa
|
||||
Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
|
||||
"""
|
||||
try:
|
||||
sign_string = (
|
||||
f"{KASSAI_SHOP_ID}:"
|
||||
f"{data.get('AMOUNT', '')}:"
|
||||
f"{KASSAI_SECRET_KEY}:"
|
||||
f"{data.get('MERCHANT_ORDER_ID', '')}"
|
||||
)
|
||||
|
||||
expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
|
||||
|
||||
result = signature.upper() == expected_signature.upper()
|
||||
|
||||
if not result:
|
||||
logger.error(f"KassaAI signature mismatch. Expected: {expected_signature}, Got: {signature}")
|
||||
logger.error(f"Sign string: {sign_string}")
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки подписи KassaAI: {e}")
|
||||
return False
|
||||
@@ -6,11 +6,14 @@ from config import TBLOCKER_WEBHOOK_PATH
|
||||
|
||||
from .tblocker import tblocker_webhook
|
||||
from .wata_payment import wata_payment_webhook
|
||||
from .kassai_payment import kassai_payment_webhook
|
||||
|
||||
|
||||
WATA_WEBHOOK_PATH = "/wata/webhook"
|
||||
KASSAI_WEBHOOK_PATH = "/kassai/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)
|
||||
router.add_post(KASSAI_WEBHOOK_PATH, kassai_payment_webhook)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import hashlib
|
||||
import json
|
||||
from aiohttp import web
|
||||
from database import add_payment, async_session_maker, update_balance
|
||||
from handlers.payments.utils import send_payment_success_notification
|
||||
from handlers.payments.kassai import verify_kassai_signature
|
||||
from config import KASSAI_SECRET_KEY, KASSAI_SHOP_ID
|
||||
from logger import logger
|
||||
|
||||
processed_payments = set()
|
||||
|
||||
|
||||
async def kassai_payment_webhook(request: web.Request):
|
||||
"""
|
||||
Обработчик вебхука от KassaAI для подтверждения оплаты
|
||||
"""
|
||||
try:
|
||||
data = await request.post()
|
||||
data_dict = dict(data)
|
||||
|
||||
logger.info(f"KassaAI webhook received from {request.remote}")
|
||||
|
||||
signature = data_dict.get("SIGN")
|
||||
if not signature:
|
||||
logger.error("KassaAI: Missing SIGN in request")
|
||||
return web.Response(status=400, text="Signature missing")
|
||||
|
||||
if not verify_kassai_webhook_signature(data_dict, signature):
|
||||
logger.error("KassaAI: Invalid signature")
|
||||
return web.Response(status=400, text="Invalid signature")
|
||||
|
||||
merchant_order_id = data_dict.get("MERCHANT_ORDER_ID")
|
||||
amount = data_dict.get("AMOUNT")
|
||||
p_email = data_dict.get("P_EMAIL")
|
||||
|
||||
if not amount:
|
||||
logger.error(f"KassaAI: Missing AMOUNT")
|
||||
return web.Response(status=400, text="Missing required fields")
|
||||
|
||||
if merchant_order_id and merchant_order_id in processed_payments:
|
||||
logger.warning(f"KassaAI: Duplicate payment {merchant_order_id}")
|
||||
return web.Response(status=200, text="YES")
|
||||
|
||||
try:
|
||||
if p_email and "@" in p_email:
|
||||
tg_id = int(p_email.split("@")[0])
|
||||
else:
|
||||
logger.error(f"KassaAI: Invalid email format: {p_email}")
|
||||
return web.Response(status=400, text="Invalid email format")
|
||||
|
||||
amount_float = float(amount)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"KassaAI: Invalid data format: {e}")
|
||||
return web.Response(status=400, text="Invalid data format")
|
||||
|
||||
async with async_session_maker() as session:
|
||||
await update_balance(session, tg_id, amount_float)
|
||||
await send_payment_success_notification(tg_id, amount_float, session)
|
||||
await add_payment(session, tg_id, amount_float, "kassai")
|
||||
|
||||
if merchant_order_id:
|
||||
processed_payments.add(merchant_order_id)
|
||||
|
||||
logger.info(f"✅ KassaAI: Payment processed for user {tg_id}, amount {amount_float}")
|
||||
return web.Response(status=200, text="YES")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"KassaAI webhook error: {e}")
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
def verify_kassai_webhook_signature(data: dict, signature: str) -> bool:
|
||||
"""
|
||||
Проверка подписи вебхука KassaAI согласно документации FreeKassa
|
||||
"""
|
||||
try:
|
||||
sign_string = (
|
||||
f"{data.get('MERCHANT_ID', '')}:"
|
||||
f"{data.get('AMOUNT', '')}:"
|
||||
f"{KASSAI_SECRET_KEY}:"
|
||||
f"{data.get('MERCHANT_ORDER_ID', '')}"
|
||||
)
|
||||
|
||||
expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
|
||||
result = signature.upper() == expected_signature.upper()
|
||||
|
||||
if not result:
|
||||
logger.error(f"KassaAI webhook signature mismatch")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"KassaAI signature verification error: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user