Clear code
This commit is contained in:
+7
-7
@@ -65,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:
|
||||
@@ -73,10 +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 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 len(payment_handlers) == 1:
|
||||
await callback_query.answer()
|
||||
@@ -88,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:
|
||||
@@ -96,9 +99,6 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
|
||||
builder.row(InlineKeyboardButton(text=ROBOKASSA, callback_data="pay_robokassa"))
|
||||
if FREEKASSA_ENABLE:
|
||||
builder.row(InlineKeyboardButton(text=FREEKASSA, callback_data="pay_freekassa"))
|
||||
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 WATA_RU_ENABLE:
|
||||
builder.row(InlineKeyboardButton(text=WATA_RU, callback_data="pay_wata_ru"))
|
||||
if WATA_SBP_ENABLE:
|
||||
|
||||
+11
-45
@@ -226,7 +226,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
|
||||
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
parts = callback_query.data.split("|")
|
||||
method_name = parts[1]
|
||||
# Теперь сумма приходит прямо в parts[2]
|
||||
amount_str = parts[2]
|
||||
|
||||
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
|
||||
@@ -276,101 +275,72 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) ->
|
||||
"""
|
||||
Создание заказа в KassaAI и получение ссылки на оплату
|
||||
"""
|
||||
# Генерируем уникальный nonce (timestamp в секундах)
|
||||
nonce = int(time.time())
|
||||
|
||||
# Генерируем уникальный paymentId для отслеживания заказа
|
||||
unique_payment_id = f"{nonce}_{tg_id}"
|
||||
|
||||
# Формируем URL с query параметрами (включая paymentId)
|
||||
url = f"https://api.fk.life/v1/orders/create?paymentId={unique_payment_id}"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Формируем email клиента (тг_id@домен)
|
||||
client_email = f"{tg_id}@{KASSAI_DOMAIN}"
|
||||
client_ip = KASSAI_IP
|
||||
|
||||
# Используем IP сервера из конфигурации
|
||||
client_ip = KASSAI_IP # IP сервера
|
||||
|
||||
# Подготавливаем ТОЛЬКО обязательные данные для подписи и запроса
|
||||
# Согласно документации: shopId, nonce, i, email, ip, amount, currency
|
||||
data_for_signature = {
|
||||
"shopId": KASSAI_SHOP_ID,
|
||||
"nonce": nonce,
|
||||
"i": method["method"],
|
||||
"email": client_email,
|
||||
"ip": client_ip,
|
||||
"amount": int(amount), # Целое число
|
||||
"amount": int(amount),
|
||||
"currency": "RUB"
|
||||
}
|
||||
|
||||
# Формируем подпись согласно документации PHP примера:
|
||||
# ksort($data); hash_hmac('sha256', implode('|', $data), $api_key)
|
||||
sorted_keys = sorted(data_for_signature.keys())
|
||||
values = [str(data_for_signature[key]) for key in sorted_keys]
|
||||
sign_string = "|".join(values)
|
||||
|
||||
# Отладочная информация
|
||||
logger.info(f"KassaAI signature debug (correct algorithm):")
|
||||
logger.info(f"Sorted keys: {sorted_keys}")
|
||||
logger.info(f"Values: {values}")
|
||||
logger.info(f"Sign string: {sign_string}")
|
||||
logger.info(f"API key length: {len(KASSAI_API_KEY)}")
|
||||
logger.info(f"Sending to URL: {url}")
|
||||
|
||||
# HMAC SHA256 как в PHP примере
|
||||
signature = hmac.new(
|
||||
KASSAI_API_KEY.encode('utf-8'),
|
||||
sign_string.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
logger.info(f"Generated signature: {signature}")
|
||||
|
||||
# Данные для отправки (только обязательные поля + подпись)
|
||||
data = data_for_signature.copy()
|
||||
data["signature"] = signature
|
||||
|
||||
logger.info(f"Sending data: {data}")
|
||||
|
||||
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()
|
||||
logger.info(f"KassaAI API response: {resp_json}")
|
||||
|
||||
# API возвращает успешный ответ со ссылкой в поле location
|
||||
if resp_json.get("type") == "success":
|
||||
payment_url = resp_json.get("location")
|
||||
if payment_url:
|
||||
logger.info(f"KassaAI payment URL created: {payment_url}")
|
||||
logger.info(f"KassaAI payment URL created for user {tg_id}")
|
||||
return payment_url
|
||||
else:
|
||||
logger.error(f"KassaAI: Нет поля location в успешном ответе: {resp_json}")
|
||||
logger.error(f"KassaAI: No location in response: {resp_json}")
|
||||
return "https://fk.life/"
|
||||
else:
|
||||
logger.error(f"KassaAI: Неуспешный ответ: {resp_json}")
|
||||
logger.error(f"KassaAI: Unsuccessful response: {resp_json}")
|
||||
return "https://fk.life/"
|
||||
except Exception as e:
|
||||
logger.error(f"KassaAI: Ошибка при разборе JSON ответа: {e}")
|
||||
logger.error(f"KassaAI: Error parsing JSON response: {e}")
|
||||
text = await resp.text()
|
||||
logger.error(f"KassaAI: Содержимое ответа: {text}")
|
||||
logger.error(f"KassaAI: Response content: {text}")
|
||||
return "https://fk.life/"
|
||||
else:
|
||||
try:
|
||||
error_json = await resp.json()
|
||||
logger.error(f"Ошибка KassaAI API: статус={resp.status}, ответ={error_json}")
|
||||
logger.error(f"KassaAI API error: status={resp.status}, response={error_json}")
|
||||
except Exception:
|
||||
text = await resp.text()
|
||||
logger.error(f"Ошибка KassaAI API: статус={resp.status}, не-JSON ответ: {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"Ошибка при создании заказа KassaAI: {e}")
|
||||
logger.error(f"Error creating KassaAI order: {e}")
|
||||
return "https://fk.life/"
|
||||
|
||||
|
||||
@@ -380,19 +350,15 @@ def verify_kassai_signature(data: dict, signature: str) -> bool:
|
||||
Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
|
||||
"""
|
||||
try:
|
||||
# Формируем строку для подписи согласно документации:
|
||||
# MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
|
||||
sign_string = (
|
||||
f"{KASSAI_SHOP_ID}:"
|
||||
f"{data.get('AMOUNT', '')}:"
|
||||
f"{KASSAI_SECRET_KEY}:" # Используем SECRET_KEY2 для вебхуков
|
||||
f"{KASSAI_SECRET_KEY}:"
|
||||
f"{data.get('MERCHANT_ORDER_ID', '')}"
|
||||
)
|
||||
|
||||
# Вычисляем MD5 хеш
|
||||
expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
|
||||
|
||||
# Сравниваем подписи (регистронезависимо)
|
||||
result = signature.upper() == expected_signature.upper()
|
||||
|
||||
if not result:
|
||||
|
||||
+13
-53
@@ -7,128 +7,88 @@ 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 для подтверждения оплаты
|
||||
Формат: MERCHANT_ID=123&AMOUNT=100&intid=123456&MERCHANT_ORDER_ID=test_order&SIGN=...
|
||||
"""
|
||||
try:
|
||||
# Получаем данные из POST запроса (form-data)
|
||||
data = await request.post()
|
||||
data_dict = dict(data)
|
||||
|
||||
# Логируем входящий запрос
|
||||
logger.info(f"KassaAI webhook received: {data_dict}")
|
||||
logger.info(f"KassaAI webhook received from {request.remote}")
|
||||
|
||||
# Получаем подпись из параметров
|
||||
signature = data_dict.get("SIGN")
|
||||
|
||||
if not signature:
|
||||
logger.error("KassaAI: Нет подписи SIGN в запросе!")
|
||||
logger.error("KassaAI: Missing SIGN in request")
|
||||
return web.Response(status=400, text="Signature missing")
|
||||
|
||||
# Проверяем подпись согласно документации KassaAI
|
||||
if not verify_kassai_webhook_signature(data_dict, signature):
|
||||
logger.error("KassaAI: Подпись не прошла проверку!")
|
||||
logger.error("KassaAI: Invalid signature")
|
||||
return web.Response(status=400, text="Invalid signature")
|
||||
|
||||
# Логируем все важные параметры
|
||||
logger.info(
|
||||
f"KassaAI webhook details: "
|
||||
f"MERCHANT_ID={data_dict.get('MERCHANT_ID')}, "
|
||||
f"AMOUNT={data_dict.get('AMOUNT')}, "
|
||||
f"intid={data_dict.get('intid')}, "
|
||||
f"MERCHANT_ORDER_ID={data_dict.get('MERCHANT_ORDER_ID')}, "
|
||||
f"P_EMAIL={data_dict.get('P_EMAIL')}, "
|
||||
f"CUR_ID={data_dict.get('CUR_ID')}, "
|
||||
f"SIGN={data_dict.get('SIGN')}"
|
||||
)
|
||||
|
||||
# Получаем необходимые данные
|
||||
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: отсутствует AMOUNT: {data_dict}")
|
||||
logger.error(f"KassaAI: Missing AMOUNT")
|
||||
return web.Response(status=400, text="Missing required fields")
|
||||
|
||||
# Проверяем уникальность платежа по MERCHANT_ORDER_ID (если есть)
|
||||
if merchant_order_id and merchant_order_id in processed_payments:
|
||||
logger.warning(f"KassaAI: платеж MERCHANT_ORDER_ID={merchant_order_id} уже был обработан. Игнорируем дубликат.")
|
||||
return web.Response(status=200, text="YES") # Возвращаем успех чтобы не было повторных попыток
|
||||
logger.warning(f"KassaAI: Duplicate payment {merchant_order_id}")
|
||||
return web.Response(status=200, text="YES")
|
||||
|
||||
try:
|
||||
# Извлекаем tg_id из email (формат: tg_id@domain)
|
||||
if p_email and "@" in p_email:
|
||||
tg_id = int(p_email.split("@")[0])
|
||||
logger.info(f"KassaAI: извлечен tg_id={tg_id} из email={p_email}")
|
||||
else:
|
||||
logger.error(f"KassaAI: некорректный email для извлечения tg_id: {p_email}")
|
||||
logger.error(f"KassaAI: Invalid email format: {p_email}")
|
||||
return web.Response(status=400, text="Invalid email format")
|
||||
|
||||
amount_float = float(amount)
|
||||
logger.info(f"KassaAI: обрабатываем платеж MERCHANT_ORDER_ID={merchant_order_id}, tg_id={tg_id}, amount={amount_float}")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"KassaAI: некорректные данные tg_id или amount: {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")
|
||||
|
||||
# Добавляем MERCHANT_ORDER_ID в список обработанных платежей (если есть)
|
||||
if merchant_order_id:
|
||||
processed_payments.add(merchant_order_id)
|
||||
|
||||
logger.info(f"✅ KassaAI: баланс пополнен для пользователя {tg_id} на {amount_float}, MERCHANT_ORDER_ID={merchant_order_id}")
|
||||
|
||||
# Возвращаем ответ согласно документации KassaAI
|
||||
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: {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
|
||||
Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
|
||||
"""
|
||||
try:
|
||||
# Формируем строку для подписи согласно документации FreeKassa:
|
||||
# MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
|
||||
sign_string = (
|
||||
f"{data.get('MERCHANT_ID', '')}:"
|
||||
f"{data.get('AMOUNT', '')}:"
|
||||
f"{KASSAI_SECRET_KEY}:" # Используем SECRET_KEY2 для вебхуков
|
||||
f"{KASSAI_SECRET_KEY}:"
|
||||
f"{data.get('MERCHANT_ORDER_ID', '')}"
|
||||
)
|
||||
|
||||
# Вычисляем MD5 хеш
|
||||
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:")
|
||||
logger.error(f"Expected: {expected_signature}")
|
||||
logger.error(f"Received: {signature}")
|
||||
logger.error(f"Sign string: {sign_string}")
|
||||
logger.error(f"MERCHANT_ID: {data.get('MERCHANT_ID')}")
|
||||
logger.error(f"AMOUNT: {data.get('AMOUNT')}")
|
||||
logger.error(f"MERCHANT_ORDER_ID: {data.get('MERCHANT_ORDER_ID')}")
|
||||
logger.error(f"KassaAI webhook signature mismatch")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки подписи KassaAI webhook: {e}")
|
||||
logger.error(f"KassaAI signature verification error: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user