FIX "Приведение Касс к Стандарту бота"

Heleket и KassaAI
This commit is contained in:
Boris Kovalskii
2025-09-15 22:53:48 +00:00
committed by GitHub
parent b0482cca1a
commit 752292cb4a
11 changed files with 1257 additions and 21 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ from .cryptobot import router as cryptobot_router
from .fast_payment_flow import router as fast_payment_flow_router
from .freekassa.freekassa_pay import router as freekassa_router
from .gift import router as gift_router
from .heleket.heleket import router as heleket_router
from .kassai.kassai import router as kassai_router
from .heleket import router as heleket_router
from .kassai import router as kassai_router
from .pay import router as pay_router
from .robokassa import router as robokassa_router
from .stars import router as stars_router
+5 -1
View File
@@ -39,7 +39,11 @@ async def _run_provider_flow(
if not fast_name:
return False
module_name = f"handlers.payments.{up.lower()}.handlers"
# Специальная логика для KassaI - оба метода в одном модуле
if up.startswith("KASSAI_"):
module_name = "handlers.payments.kassai.handlers"
else:
module_name = f"handlers.payments.{up.lower()}.handlers"
try:
module = importlib.import_module(module_name)
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+85
View File
@@ -0,0 +1,85 @@
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from sqlalchemy.ext.asyncio import AsyncSession
from handlers.buttons import PAY_2, MAIN_MENU
from handlers.texts import DEFAULT_PAYMENT_MESSAGE
from handlers.utils import edit_or_send_message
from handlers.payments.currency_rates import format_for_user
from database import get_temporary_data
from logger import logger
from .service import HELEKET_PAYMENT_METHODS, generate_heleket_payment_link, process_callback_pay_heleket
from .service import router as service_router
router = Router(name="heleket_router")
# Подключаем роутер из service.py для обработки всех callback'ов
router.include_router(service_router)
@router.callback_query(F.data == "pay_heleket_crypto")
async def handle_pay_heleket_crypto(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_heleket(callback_query, state, session, method_name="crypto")
async def handle_custom_amount_input_heleket(
event,
session: AsyncSession,
pay_button_text: str = PAY_2,
main_menu_text: str = MAIN_MENU,
):
"""
Функция быстрого потока для Heleket - принимает недостающую сумму и формирует платеж.
Работает с временными данными из fast_payment_flow для создания/продления/подарка.
"""
message = event.message
from_user = event.from_user
tg_id = from_user.id
temp_data = await get_temporary_data(session, tg_id)
if not temp_data or temp_data["state"] not in ["waiting_for_payment", "waiting_for_renewal_payment", "waiting_for_gift_payment"]:
await edit_or_send_message(target_message=message, text="❌ Не удалось получить данные для оплаты.")
return
amount = int(temp_data["data"].get("required_amount", 0))
if amount <= 0:
await edit_or_send_message(target_message=message, text="❌ Не удалось определить сумму оплаты.")
return
# Проверяем минимальную сумму для криптоплатежей
if amount < 10:
await edit_or_send_message(target_message=message, text="❌ Минимальная сумма для оплаты криптовалютой — 10 рублей.")
return
# Выбираем единственный доступный метод Heleket (crypto)
enabled_methods = [m for m in HELEKET_PAYMENT_METHODS if m["enable"]]
if not enabled_methods:
await edit_or_send_message(target_message=message, text="❌ Способ оплаты Heleket временно недоступен.")
return
method = enabled_methods[0] # Всегда crypto
try:
payment_url = await generate_heleket_payment_link(amount, tg_id, method)
markup = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
]
)
language_code = getattr(from_user, "language_code", None)
# Для Heleket показываем сумму в рублях, но платёж идёт в USD
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="RUB")
text_out = DEFAULT_PAYMENT_MESSAGE.format(amount=amount_text)
await edit_or_send_message(target_message=message, text=text_out, reply_markup=markup)
except Exception as e:
logger.error(f"Ошибка при создании платежа Heleket для пользователя {tg_id}: {e}")
await edit_or_send_message(
target_message=message,
text="Произошла ошибка при создании платежа. Попробуйте позже.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
)
+452
View File
@@ -0,0 +1,452 @@
import base64
import hashlib
import json
import time
from decimal import Decimal, ROUND_HALF_UP
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 config import (
HELEKET_API_KEY,
HELEKET_CALLBACK_URL,
HELEKET_MERCHANT_ID,
HELEKET_RETURN_URL,
HELEKET_SUCCESS_URL,
PROVIDERS_ENABLED,
)
from handlers.payments.providers import get_providers
from ..currency_rates import get_rub_rate
from handlers.buttons import BACK, HELEKET, PAY_2
from handlers.texts import ENTER_SUM, HELEKET_CRYPTO_DESCRIPTION, HELEKET_PAYMENT_MESSAGE, PAYMENT_OPTIONS
from handlers.utils import edit_or_send_message
from database import add_payment, async_session_maker
from logger import logger
router = Router()
class ReplenishBalanceHeleket(StatesGroup):
choosing_method = State()
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
PROVIDERS = get_providers(PROVIDERS_ENABLED)
HELEKET_PAYMENT_METHODS = [
{
"enable": bool(PROVIDERS.get("HELEKET", {}).get("enabled")),
"currency": (PROVIDERS.get("HELEKET", {}).get("currency") or "USD"),
"to_currency": None,
"name": "crypto",
"button": HELEKET,
"desc": HELEKET_CRYPTO_DESCRIPTION,
},
]
async def process_callback_pay_heleket(
callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, method_name: str = None
):
try:
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Heleket payment.")
await state.clear()
if not method_name:
enabled_methods = [m["name"] for m in HELEKET_PAYMENT_METHODS if m["enable"]]
if len(enabled_methods) == 1:
method_name = enabled_methods[0]
if method_name:
method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None)
if not method:
try:
await callback_query.message.delete()
except Exception:
pass
await callback_query.message.answer(
text="Ошибка: выбранный способ оплаты недоступен.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
)
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"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]['callback_data'].split('|')[1]}",
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
)
)
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"heleket_custom_amount|{method_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
try:
await callback_query.message.delete()
except Exception:
pass
new_msg = await callback_query.message.answer(
text=method["desc"],
reply_markup=builder.as_markup(),
)
await state.update_data(
heleket_method=method_name,
message_id=new_msg.message_id,
chat_id=new_msg.chat.id,
)
await state.set_state(ReplenishBalanceHeleket.choosing_amount)
return
builder = InlineKeyboardBuilder()
for method in HELEKET_PAYMENT_METHODS:
if method["enable"]:
builder.row(
InlineKeyboardButton(text=method["button"], callback_data=f"heleket_method|{method['name']}")
)
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
try:
await callback_query.message.delete()
except Exception:
pass
new_msg = await callback_query.message.answer(
text="Выберите способ оплаты через Heleket:",
reply_markup=builder.as_markup(),
)
await state.update_data(message_id=new_msg.message_id, chat_id=new_msg.chat.id)
await state.set_state(ReplenishBalanceHeleket.choosing_method)
except Exception as e:
logger.error(f"Error in process_callback_pay_heleket for user {callback_query.message.chat.id}: {e}")
await callback_query.answer("Произошла ошибка при инициализации платежа. Попробуйте позже.", show_alert=True)
@router.callback_query(F.data.startswith("heleket_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 HELEKET_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=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
await state.update_data(heleket_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"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]['callback_data'].split('|')[1]}",
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
)
)
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"heleket_custom_amount|{method_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay"))
await edit_or_send_message(
target_message=callback_query.message,
text=method["desc"],
reply_markup=builder.as_markup(),
force_text=True,
)
await state.update_data(message_id=callback_query.message.message_id, chat_id=callback_query.message.chat.id)
await state.set_state(ReplenishBalanceHeleket.choosing_amount)
@router.callback_query(F.data.startswith("heleket_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(heleket_method=method_name)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_heleket_crypto"))
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(ReplenishBalanceHeleket.entering_custom_amount)
@router.message(ReplenishBalanceHeleket.entering_custom_amount)
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
data = await state.get_data()
method_name = data.get("heleket_method")
method = next((m for m in HELEKET_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=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
try:
amount = int(message.text.strip())
if amount <= 0:
raise ValueError
if amount < 10:
await edit_or_send_message(
target_message=message,
text="Минимальная сумма для оплаты криптовалютой — 10 рублей.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
except Exception:
await edit_or_send_message(
target_message=message,
text="Некорректная сумма. Введите целое число больше 0.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
await state.update_data(amount=amount)
payment_url = await generate_heleket_payment_link(amount, message.chat.id, method)
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")],
]
)
await edit_or_send_message(
target_message=message,
text=HELEKET_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard,
force_text=True,
)
await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation)
async def process_fast_flow_heleket(
callback_query: types.CallbackQuery,
state: FSMContext,
session: AsyncSession,
amount: int,
method_name: str = "crypto",
):
method = next((m for m in HELEKET_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=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
if amount <= 0:
await edit_or_send_message(
target_message=callback_query.message,
text="Некорректная сумма.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
if amount < 10:
await edit_or_send_message(
target_message=callback_query.message,
text="Минимальная сумма для оплаты криптовалютой — 10 рублей.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
await state.update_data(heleket_method=method_name, amount=amount)
payment_url = await generate_heleket_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="balance")],
]
)
await edit_or_send_message(
target_message=callback_query.message,
text=HELEKET_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard,
force_text=True,
)
await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation)
@router.callback_query(F.data.startswith("heleket_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 HELEKET_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=InlineKeyboardMarkup(inline_keyboard=[]),
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=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
await state.update_data(amount=amount)
payment_url = await generate_heleket_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="balance")],
]
)
await edit_or_send_message(
target_message=callback_query.message,
text=HELEKET_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard,
force_text=True,
)
await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation)
async def generate_heleket_payment_link(amount: int, tg_id: int, method: dict) -> str:
"""
Создание платежа в Heleket и получение ссылки на оплату.
amount — сумма в RUB, method['currency'] — валюта провайдера (обычно USD).
"""
url = "https://api.heleket.com/v1/payment"
unique_order_id = f"{int(time.time())}_{tg_id}"
try:
async with aiohttp.ClientSession() as session:
pay_cur = str(method["currency"]).upper()
if pay_cur == "RUB":
payment_amount = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
else:
rate = await get_rub_rate(pay_cur, session=session)
payment_amount = (Decimal(str(amount)) * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
async with async_session_maker() as dbs:
await add_payment(
session=dbs,
tg_id=tg_id,
amount=float(amount),
payment_system="HELEKET",
status="pending",
currency="RUB",
payment_id=unique_order_id,
)
data = {
"amount": str(payment_amount),
"currency": method["currency"],
"order_id": unique_order_id,
"url_success": HELEKET_SUCCESS_URL,
"url_return": HELEKET_RETURN_URL,
"url_callback": HELEKET_CALLBACK_URL,
"additional_data": f"tg_id:{tg_id},rub_amount:{amount}",
}
if method.get("to_currency"):
data["to_currency"] = method["to_currency"]
json_data = json.dumps(data, separators=(",", ":"))
base64_data = base64.b64encode(json_data.encode("utf-8")).decode("utf-8")
sign_string = base64_data + HELEKET_API_KEY
signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest()
headers = {
"merchant": HELEKET_MERCHANT_ID,
"sign": signature,
"Content-Type": "application/json",
}
async with session.post(url, headers=headers, data=json_data, timeout=60) as resp:
if resp.status == 200:
try:
resp_json = await resp.json()
if resp_json.get("state") == 0:
payment_url = resp_json.get("result", {}).get("url")
if payment_url:
logger.info(f"Heleket payment URL created for user {tg_id}")
return payment_url
else:
logger.error(f"Heleket: No URL in response: {resp_json}")
return "https://heleket.com/"
else:
logger.error(f"Heleket: Unsuccessful response: {resp_json}")
return "https://heleket.com/"
except Exception as e:
logger.error(f"Heleket: Error parsing JSON response: {e}")
text = await resp.text()
logger.error(f"Heleket: Response content: {text}")
return "https://heleket.com/"
else:
try:
error_json = await resp.json()
logger.error(f"Heleket API error: status={resp.status}, response={error_json}")
except Exception:
text = await resp.text()
logger.error(f"Heleket API error: status={resp.status}, non-JSON response: {text}")
return "https://heleket.com/"
except Exception as e:
logger.error(f"Error creating Heleket payment: {e}")
return "https://heleket.com/"
+97
View File
@@ -0,0 +1,97 @@
import base64
import hashlib
import json
from logger import logger
from config import HELEKET_API_KEY
from database import async_session_maker, update_payment_status, add_balance_to_user
def verify_heleket_signature(data: dict) -> bool:
try:
received_signature = data.get('sign')
if not received_signature:
logger.error("Heleket webhook: отсутствует подпись")
return False
data_without_sign = data.copy()
del data_without_sign['sign']
json_data = json.dumps(data_without_sign, ensure_ascii=False, separators=(',', ':'))
json_data = json_data.replace('/', '\\/')
base64_data = base64.b64encode(json_data.encode('utf-8')).decode('utf-8')
sign_string = base64_data + HELEKET_API_KEY
calculated_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
is_valid = calculated_signature.lower() == received_signature.lower()
if not is_valid:
logger.error(f"Heleket webhook: неверная подпись. Ожидалось: {calculated_signature}, получено: {received_signature}")
logger.error(f"Heleket webhook: строка для подписи: {sign_string}")
else:
logger.info("Heleket webhook: подпись успешно проверена")
return is_valid
except Exception as e:
logger.error(f"Ошибка проверки подписи Heleket webhook: {e}")
return False
async def process_heleket_webhook(data: dict) -> bool:
try:
logger.info(f"Processing Heleket webhook: {data}")
webhook_type = data.get('type')
uuid = data.get('uuid')
order_id = data.get('order_id')
status = data.get('status')
amount = data.get('amount')
payment_amount = data.get('payment_amount')
merchant_amount = data.get('merchant_amount')
currency = data.get('currency')
payer_currency = data.get('payer_currency')
additional_data = data.get('additional_data')
is_final = data.get('is_final', False)
logger.info(f"Heleket webhook - Type: {webhook_type}, UUID: {uuid}, Order: {order_id}, Status: {status}")
if webhook_type != 'payment':
logger.warning(f"Heleket webhook: неизвестный тип {webhook_type}")
return False
if status in ['paid', 'paid_over']:
logger.info(f"Heleket: успешный платёж {order_id} на сумму {payment_amount} {payer_currency}")
tg_id = None
rub_amount = None
if additional_data:
try:
for part in additional_data.split(','):
if part.startswith('tg_id:'):
tg_id = int(part.split(':')[1])
elif part.startswith('rub_amount:'):
rub_amount = float(part.split(':')[1])
except Exception as e:
logger.error(f"Ошибка парсинга additional_data: {e}")
if not tg_id and '_' in order_id:
try:
tg_id = int(order_id.split('_')[1])
except Exception as e:
logger.error(f"Ошибка извлечения tg_id из order_id: {e}")
if not tg_id:
logger.error(f"Не удалось извлечь tg_id из Heleket webhook: {data}")
return False
balance_amount = rub_amount if rub_amount else float(merchant_amount)
async with async_session_maker() as session:
await update_payment_status(session, order_id, "success")
await add_balance_to_user(session, tg_id, balance_amount)
await session.commit()
logger.info(f"Heleket: платёж {order_id} для пользователя {tg_id} успешно обработан, баланс пополнен на {balance_amount} RUB")
return True
elif status in ['fail', 'wrong_amount', 'cancel', 'system_fail']:
logger.warning(f"Heleket: неудачный платёж {order_id}, статус: {status}")
async with async_session_maker() as session:
await update_payment_status(session, order_id, "failed")
await session.commit()
return True
else:
logger.info(f"Heleket: промежуточный статус {status} для платежа {order_id}")
return True
except Exception as e:
logger.error(f"Ошибка обработки Heleket webhook: {e}")
return False
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+142
View File
@@ -0,0 +1,142 @@
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from sqlalchemy.ext.asyncio import AsyncSession
from handlers.buttons import PAY_2, MAIN_MENU
from handlers.texts import DEFAULT_PAYMENT_MESSAGE
from handlers.utils import edit_or_send_message
from handlers.payments.currency_rates import format_for_user
from database import get_temporary_data
from logger import logger
from .service import KASSAI_PAYMENT_METHODS, generate_kassai_payment_link, process_callback_pay_kassai
from .service import router as service_router
router = Router(name="kassai_router")
# Подключаем роутер из service.py для обработки всех callback'ов
router.include_router(service_router)
@router.callback_query(F.data == "pay_kassai_cards")
async def handle_pay_kassai_cards(callback_query: types.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: types.CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_kassai(callback_query, state, session, method_name="sbp")
async def handle_custom_amount_input_kassai_cards(
event,
session: AsyncSession,
pay_button_text: str = PAY_2,
main_menu_text: str = MAIN_MENU,
):
"""
Функция быстрого потока для KassaI Cards - принимает недостающую сумму и формирует платеж картами.
Работает с временными данными из fast_payment_flow для создания/продления/подарка.
"""
message = event.message
from_user = event.from_user
tg_id = from_user.id
temp_data = await get_temporary_data(session, tg_id)
if not temp_data or temp_data["state"] not in ["waiting_for_payment", "waiting_for_renewal_payment", "waiting_for_gift_payment"]:
await edit_or_send_message(target_message=message, text="❌ Не удалось получить данные для оплаты.")
return
amount = int(temp_data["data"].get("required_amount", 0))
if amount <= 0:
await edit_or_send_message(target_message=message, text="❌ Не удалось определить сумму оплаты.")
return
# Используем метод карт
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == "cards" and m["enable"]), None)
if not method:
await edit_or_send_message(target_message=message, text="❌ Оплата картами KassaAI временно недоступна.")
return
try:
payment_url = await generate_kassai_payment_link(amount, tg_id, method)
markup = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
]
)
language_code = getattr(from_user, "language_code", None)
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="RUB")
text_out = DEFAULT_PAYMENT_MESSAGE.format(amount=amount_text)
await edit_or_send_message(target_message=message, text=text_out, reply_markup=markup)
except Exception as e:
logger.error(f"Ошибка при создании платежа KassaAI Cards для пользователя {tg_id}: {e}")
await edit_or_send_message(
target_message=message,
text="Произошла ошибка при создании платежа. Попробуйте позже.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
)
async def handle_custom_amount_input_kassai_sbp(
event,
session: AsyncSession,
pay_button_text: str = PAY_2,
main_menu_text: str = MAIN_MENU,
):
"""
Функция быстрого потока для KassaI SBP - принимает недостающую сумму и формирует платеж через СБП.
Работает с временными данными из fast_payment_flow для создания/продления/подарка.
"""
message = event.message
from_user = event.from_user
tg_id = from_user.id
temp_data = await get_temporary_data(session, tg_id)
if not temp_data or temp_data["state"] not in ["waiting_for_payment", "waiting_for_renewal_payment", "waiting_for_gift_payment"]:
await edit_or_send_message(target_message=message, text="❌ Не удалось получить данные для оплаты.")
return
amount = int(temp_data["data"].get("required_amount", 0))
if amount <= 0:
await edit_or_send_message(target_message=message, text="❌ Не удалось определить сумму оплаты.")
return
# Проверяем минимальную сумму для СБП
if amount < 10:
await edit_or_send_message(target_message=message, text="❌ Минимальная сумма для оплаты через СБП — 10 рублей.")
return
# Используем метод СБП
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == "sbp" and m["enable"]), None)
if not method:
await edit_or_send_message(target_message=message, text="❌ Оплата через СБП KassaAI временно недоступна.")
return
try:
payment_url = await generate_kassai_payment_link(amount, tg_id, method)
markup = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
]
)
language_code = getattr(from_user, "language_code", None)
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="RUB")
text_out = DEFAULT_PAYMENT_MESSAGE.format(amount=amount_text)
await edit_or_send_message(target_message=message, text=text_out, reply_markup=markup)
except Exception as e:
logger.error(f"Ошибка при создании платежа KassaAI SBP для пользователя {tg_id}: {e}")
await edit_or_send_message(
target_message=message,
text="Произошла ошибка при создании платежа. Попробуйте позже.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
)
+381
View File
@@ -0,0 +1,381 @@
import hashlib
import hmac
import time
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 config import (
KASSAI_API_KEY,
KASSAI_DOMAIN,
KASSAI_FAILURE_URL,
KASSAI_IP,
KASSAI_SECRET_KEY,
KASSAI_SHOP_ID,
KASSAI_SUCCESS_URL,
PROVIDERS_ENABLED,
)
from handlers.payments.providers import get_providers
from handlers.buttons import BACK, KASSAI_CARDS, KASSAI_SBP, PAY_2
from handlers.texts import (
ENTER_SUM,
KASSAI_CARDS_DESCRIPTION,
KASSAI_PAYMENT_MESSAGE,
KASSAI_SBP_DESCRIPTION,
PAYMENT_OPTIONS,
)
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()
PROVIDERS = get_providers(PROVIDERS_ENABLED)
KASSAI_PAYMENT_METHODS = [
{
"enable": bool(PROVIDERS.get("KASSAI_CARDS", {}).get("enabled")),
"method": 36,
"name": "cards",
"button": KASSAI_CARDS,
"desc": KASSAI_CARDS_DESCRIPTION,
},
{
"enable": bool(PROVIDERS.get("KASSAI_SBP", {}).get("enabled")),
"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
):
try:
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated KassaAI payment.")
await state.clear()
if method_name:
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None)
if not method:
try:
await callback_query.message.delete()
except Exception:
pass
await callback_query.message.answer(
text="Ошибка: выбранный способ оплаты недоступен.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
)
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="balance"))
try:
await callback_query.message.delete()
except Exception:
pass
new_msg = await callback_query.message.answer(
text=method["desc"],
reply_markup=builder.as_markup(),
)
await state.update_data(
kassai_method=method_name,
message_id=new_msg.message_id,
chat_id=new_msg.chat.id,
)
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"))
try:
await callback_query.message.delete()
except Exception:
pass
new_msg = await callback_query.message.answer(
text="Выберите способ оплаты через KassaAI:",
reply_markup=builder.as_markup(),
)
await state.update_data(message_id=new_msg.message_id, chat_id=new_msg.chat.id)
await state.set_state(ReplenishBalanceKassaiState.choosing_method)
except Exception as e:
logger.error(f"Error in process_callback_pay_kassai for user {callback_query.message.chat.id}: {e}")
await callback_query.answer("Произошла ошибка при инициализации платежа. Попробуйте позже.", show_alert=True)
@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=InlineKeyboardMarkup(inline_keyboard=[]),
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 edit_or_send_message(
target_message=callback_query.message,
text=method["desc"],
reply_markup=builder.as_markup(),
force_text=True,
)
await state.update_data(message_id=callback_query.message.message_id, chat_id=callback_query.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=InlineKeyboardMarkup(inline_keyboard=[]),
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=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
except Exception:
await edit_or_send_message(
target_message=message,
text="Некорректная сумма. Введите целое число больше 0.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
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="balance")],
]
)
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=InlineKeyboardMarkup(inline_keyboard=[]),
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=InlineKeyboardMarkup(inline_keyboard=[]),
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="balance")],
]
)
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 = "https://api.fk.life/v1/orders/create"
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",
"success_url": KASSAI_SUCCESS_URL,
"failure_url": KASSAI_FAILURE_URL,
"paymentId": unique_payment_id,
}
sign_string = "|".join(str(data_for_signature[k]) for k in sorted(data_for_signature.keys()))
signature = hmac.new(KASSAI_API_KEY.encode("utf-8"), sign_string.encode("utf-8"), hashlib.sha256).hexdigest()
data = {**data_for_signature, "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
logger.error(f"KassaAI: No location in response: {resp_json}")
return "https://fk.life/"
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/"
+87
View File
@@ -0,0 +1,87 @@
import hashlib
from aiohttp import web
from logger import logger
from config import KASSAI_SHOP_ID, KASSAI_SECRET_KEY
from database import add_payment, async_session_maker, get_payment_by_payment_id, update_balance, update_payment_status
from handlers.payments.utils import send_payment_success_notification
def verify_kassai_signature(data: dict, signature: str) -> bool:
try:
sign_string = f"{KASSAI_SHOP_ID}:{data.get('AMOUNT', '')}:{KASSAI_SECRET_KEY}:{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}")
else:
logger.info("KassaAI webhook: подпись успешно проверена")
return result
except Exception as e:
logger.error(f"Ошибка проверки подписи KassaAI: {e}")
return False
async def kassai_webhook(request: web.Request):
try:
data = await request.post()
logger.info(f"KassaAI webhook received: {dict(data)}")
signature = data.get('SIGN', '')
if not signature:
logger.error("KassaAI webhook: отсутствует подпись")
return web.Response(status=400)
if not verify_kassai_signature(data, signature):
logger.error("KassaAI webhook: неверная подпись")
return web.Response(status=400)
amount_raw = data.get('AMOUNT')
order_id = data.get('MERCHANT_ORDER_ID')
status = data.get('STATUS', '')
if not amount_raw or not order_id:
logger.error("KassaAI webhook: отсутствуют обязательные параметры")
return web.Response(status=400)
if status.upper() != 'SUCCESS':
logger.warning(f"KassaAI webhook: неуспешный статус {status} для заказа {order_id}")
return web.Response(text="OK")
amount = float(amount_raw)
try:
tg_id = int(order_id.split('_')[1])
except (IndexError, ValueError) as e:
logger.error(f"KassaAI webhook: не удалось извлечь tg_id из order_id {order_id}: {e}")
return web.Response(status=400)
logger.info(f"KassaAI: успешный платёж {order_id} на сумму {amount} RUB для пользователя {tg_id}")
async with async_session_maker() as session:
payment = await get_payment_by_payment_id(session, order_id)
if payment:
if payment.get("status") == "success":
logger.info(f"KassaAI: платёж {order_id} уже обработан")
return web.Response(text="OK")
ok = await update_payment_status(session=session, internal_id=int(payment["id"]), new_status="success")
if not ok:
logger.error(f"KassaAI: не удалось обновить статус платежа {order_id}")
return web.Response(status=500)
else:
await add_payment(
session=session,
tg_id=tg_id,
amount=amount,
payment_system="KASSAI",
status="success",
currency="RUB",
payment_id=order_id,
metadata=None,
)
await update_balance(session, tg_id, amount)
await send_payment_success_notification(tg_id, amount, session)
await session.commit()
logger.info(f"KassaAI: платёж {order_id} успешно обработан, баланс пользователя {tg_id} пополнен на {amount} RUB")
return web.Response(text="OK")
except Exception as e:
logger.error(f"Ошибка обработки KassaAI webhook: {e}")
return web.Response(status=500)
-18
View File
@@ -15,8 +15,6 @@ from database import get_last_payments
from database.models import User
from handlers import buttons as btn
from handlers.payments.heleket.heleket import process_callback_pay_heleket
from handlers.payments.kassai.kassai import process_callback_pay_kassai
from handlers.payments.stars.handlers import process_callback_pay_stars
from handlers.payments.tribute.handlers import process_callback_pay_tribute
from handlers.payments.wata.wata import process_callback_pay_wata
@@ -181,22 +179,6 @@ async def handle_pay_wata_sbp(callback_query: CallbackQuery, state: FSMContext,
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")
@router.callback_query(F.data == "pay_heleket_crypto")
async def handle_pay_heleket_crypto(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_heleket(callback_query, state, session, method_name="crypto")
@router.callback_query(F.data == "pay_tribute")
async def handle_pay_tribute(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_tribute(callback_query, state, session)