FIX "Консистентный дизайн касс"
- FIX "Консистентный дизайн касс" Heleket и Kassai - Оптимизация касс по PEP8 - Оптимизация функций KassaI
This commit is contained in:
@@ -1,25 +1,38 @@
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from handlers.buttons import PAY_2, MAIN_MENU
|
||||
from database import get_temporary_data
|
||||
from database.models import User
|
||||
from handlers.buttons import MAIN_MENU, PAY_2
|
||||
from handlers.payments.currency_rates import format_for_user
|
||||
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
|
||||
from .service import (
|
||||
HELEKET_PAYMENT_METHODS,
|
||||
generate_heleket_payment_link,
|
||||
process_callback_pay_heleket,
|
||||
router as service_router,
|
||||
)
|
||||
|
||||
router = Router(name="heleket_router")
|
||||
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_pay_heleket_crypto(
|
||||
callback_query: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
):
|
||||
"""Обработчик оплаты через Heleket криптовалютой."""
|
||||
await process_callback_pay_heleket(
|
||||
callback_query, state, session, method_name="crypto"
|
||||
)
|
||||
|
||||
|
||||
async def handle_custom_amount_input_heleket(
|
||||
@@ -28,63 +41,103 @@ async def handle_custom_amount_input_heleket(
|
||||
pay_button_text: str = PAY_2,
|
||||
main_menu_text: str = MAIN_MENU,
|
||||
):
|
||||
"""
|
||||
Функция быстрого потока для Heleket - принимает недостающую сумму и формирует платеж.
|
||||
Работает с временными данными из fast_payment_flow для создания/продления/подарка.
|
||||
"""Функция быстрого потока для 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="❌ Не удалось получить данные для оплаты.")
|
||||
valid_states = [
|
||||
"waiting_for_payment",
|
||||
"waiting_for_renewal_payment",
|
||||
"waiting_for_gift_payment",
|
||||
]
|
||||
if not temp_data or temp_data["state"] not in valid_states:
|
||||
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="❌ Не удалось определить сумму оплаты.")
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="❌ Не удалось определить сумму оплаты.",
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
if amount < 10:
|
||||
await edit_or_send_message(target_message=message, text="❌ Минимальная сумма для оплаты криптовалютой — 10₽ (≈0.1$).")
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="❌ Минимальная сумма для оплаты криптовалютой — 10₽ (≈0.1$).",
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
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 временно недоступен.")
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="❌ Способ оплаты Heleket временно недоступен.",
|
||||
)
|
||||
return
|
||||
method = enabled_methods[0]
|
||||
|
||||
try:
|
||||
payment_url = await generate_heleket_payment_link(amount, tg_id, method)
|
||||
|
||||
payment_url = await generate_heleket_payment_link(
|
||||
amount, tg_id, method
|
||||
)
|
||||
|
||||
if not payment_url or payment_url == "https://heleket.com/":
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="❌ Произошла ошибка при создании платежа. Попробуйте позже или выберите другой способ оплаты.",
|
||||
text=(
|
||||
"❌ Произошла ошибка при создании платежа. "
|
||||
"Попробуйте позже или выберите другой способ оплаты."
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
markup = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
|
||||
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=main_menu_text, callback_data="profile"
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
from database.models import User
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User.language_code).where(User.tg_id == tg_id))
|
||||
result = await session.execute(
|
||||
select(User.language_code).where(User.tg_id == tg_id)
|
||||
)
|
||||
language_code = result.scalar_one_or_none()
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code)
|
||||
amount_text = await format_for_user(
|
||||
session,
|
||||
tg_id,
|
||||
float(amount),
|
||||
language_code,
|
||||
force_currency="USD",
|
||||
)
|
||||
text_out = DEFAULT_PAYMENT_MESSAGE.format(amount=amount_text)
|
||||
|
||||
await edit_or_send_message(target_message=message, text=text_out, reply_markup=markup)
|
||||
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}")
|
||||
logger.error(
|
||||
f"Ошибка при создании платежа Heleket "
|
||||
f"для пользователя {tg_id}: {e}"
|
||||
)
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="Произошла ошибка при создании платежа. Попробуйте позже.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
import aiohttp
|
||||
from aiogram import F, Router, types
|
||||
@@ -10,8 +10,8 @@ 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 import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from handlers.payments.currency_rates import format_for_user
|
||||
|
||||
from config import (
|
||||
HELEKET_API_KEY,
|
||||
@@ -21,14 +21,28 @@ from config import (
|
||||
HELEKET_SUCCESS_URL,
|
||||
PROVIDERS_ENABLED,
|
||||
)
|
||||
from handlers.payments.providers import get_providers
|
||||
from handlers.payments.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
|
||||
from handlers.payments.keyboards import build_amounts_keyboard, payment_options_for_user, parse_amount_from_callback, pay_keyboard
|
||||
from handlers.utils import edit_or_send_message
|
||||
from database import add_payment, async_session_maker
|
||||
from database.models import User
|
||||
from handlers.buttons import BACK, HELEKET, PAY_2
|
||||
from handlers.payments.currency_rates import (
|
||||
format_for_user,
|
||||
get_rub_rate,
|
||||
pick_currency,
|
||||
to_rub,
|
||||
)
|
||||
from handlers.payments.keyboards import (
|
||||
build_amounts_keyboard,
|
||||
parse_amount_from_callback,
|
||||
pay_keyboard,
|
||||
payment_options_for_user,
|
||||
)
|
||||
from handlers.payments.providers import get_providers
|
||||
from handlers.texts import (
|
||||
ENTER_SUM,
|
||||
HELEKET_CRYPTO_DESCRIPTION,
|
||||
HELEKET_PAYMENT_MESSAGE,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -37,7 +51,6 @@ router = Router()
|
||||
|
||||
async def get_user_language(session: AsyncSession, tg_id: int) -> str | None:
|
||||
"""Получает язык пользователя из базы данных"""
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
select(User.language_code).where(User.tg_id == tg_id)
|
||||
)
|
||||
@@ -91,7 +104,9 @@ async def process_callback_pay_heleket(
|
||||
return
|
||||
|
||||
language_code = await get_user_language(session, tg_id)
|
||||
opts = await payment_options_for_user(session, tg_id, language_code)
|
||||
opts = await payment_options_for_user(
|
||||
session, tg_id, language_code, force_currency="USD"
|
||||
)
|
||||
builder = build_amounts_keyboard(
|
||||
prefix=f"heleket_{method_name}",
|
||||
pattern="{prefix}_amount|{price}",
|
||||
@@ -159,7 +174,9 @@ async def process_method_selection(callback_query: types.CallbackQuery, state: F
|
||||
tg_id = callback_query.from_user.id
|
||||
|
||||
language_code = await get_user_language(session, tg_id)
|
||||
opts = await payment_options_for_user(session, tg_id, language_code)
|
||||
opts = await payment_options_for_user(
|
||||
session, tg_id, language_code, force_currency="USD"
|
||||
)
|
||||
builder = build_amounts_keyboard(
|
||||
prefix=f"heleket_{method_name}",
|
||||
pattern="{prefix}_amount|{price}",
|
||||
@@ -186,7 +203,6 @@ async def process_custom_amount_button(callback_query: types.CallbackQuery, stat
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_heleket_crypto"))
|
||||
|
||||
from ..currency_rates import pick_currency
|
||||
language_code = await get_user_language(session, callback_query.from_user.id)
|
||||
currency = pick_currency(language_code)
|
||||
|
||||
@@ -215,7 +231,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext,
|
||||
)
|
||||
return
|
||||
|
||||
from ..currency_rates import pick_currency
|
||||
language_code = await get_user_language(session, message.from_user.id)
|
||||
currency = pick_currency(language_code)
|
||||
|
||||
@@ -244,7 +259,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext,
|
||||
)
|
||||
return
|
||||
|
||||
from ..currency_rates import to_rub
|
||||
if currency == "RUB":
|
||||
amount_rub = user_amount
|
||||
else:
|
||||
@@ -265,9 +279,8 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext,
|
||||
|
||||
confirm_keyboard = pay_keyboard(payment_url, pay_text=PAY_2, back_cb="balance")
|
||||
|
||||
from ..currency_rates import format_for_user
|
||||
tg_id = message.from_user.id
|
||||
amount_text = await format_for_user(session, tg_id, float(amount_rub), language_code)
|
||||
amount_text = await format_for_user(session, tg_id, float(amount_rub), language_code, force_currency="USD")
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
@@ -328,7 +341,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
|
||||
|
||||
tg_id = callback_query.from_user.id
|
||||
language_code = await get_user_language(session, tg_id)
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code)
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="USD")
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
|
||||
@@ -1,32 +1,58 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from aiohttp import web
|
||||
from logger import logger
|
||||
|
||||
from config import HELEKET_API_KEY
|
||||
from database import async_session_maker, update_payment_status, update_balance, add_payment, get_payment_by_payment_id
|
||||
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
|
||||
from logger import logger
|
||||
|
||||
|
||||
def verify_heleket_signature(data: dict) -> bool:
|
||||
"""Проверяет подпись webhook от Heleket.
|
||||
|
||||
Args:
|
||||
data: Данные от webhook
|
||||
|
||||
Returns:
|
||||
True если подпись валидна, иначе False
|
||||
"""
|
||||
try:
|
||||
received_signature = data.get('sign')
|
||||
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')
|
||||
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()
|
||||
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: неверная подпись. "
|
||||
f"Ожидалось: {calculated_signature}, "
|
||||
f"получено: {received_signature}"
|
||||
)
|
||||
logger.error(f"Heleket webhook: строка для подписи: {sign_string}")
|
||||
else:
|
||||
logger.info("Heleket webhook: подпись успешно проверена")
|
||||
@@ -37,59 +63,86 @@ def verify_heleket_signature(data: dict) -> bool:
|
||||
|
||||
|
||||
async def process_heleket_webhook(data: dict) -> bool:
|
||||
"""Обрабатывает webhook от Heleket.
|
||||
|
||||
Args:
|
||||
data: Данные от webhook
|
||||
|
||||
Returns:
|
||||
True если обработка успешна, иначе False
|
||||
"""
|
||||
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':
|
||||
|
||||
webhook_type = data.get("type")
|
||||
order_id = data.get("order_id")
|
||||
status = data.get("status")
|
||||
payment_amount = data.get("payment_amount")
|
||||
merchant_amount = data.get("merchant_amount")
|
||||
payer_currency = data.get("payer_currency")
|
||||
additional_data = data.get("additional_data")
|
||||
|
||||
logger.info(
|
||||
f"Heleket webhook - Type: {webhook_type}, "
|
||||
f"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}")
|
||||
if status in ["paid", "paid_over"]:
|
||||
logger.info(
|
||||
f"Heleket: успешный платёж {order_id} на сумму "
|
||||
f"{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])
|
||||
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:
|
||||
if not tg_id and "_" in order_id:
|
||||
try:
|
||||
tg_id = int(order_id.split('_')[1])
|
||||
tg_id = int(order_id.split("_")[1])
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка извлечения tg_id из order_id: {e}")
|
||||
logger.error(
|
||||
f"Ошибка извлечения tg_id из order_id: {e}"
|
||||
)
|
||||
if not tg_id:
|
||||
logger.error(f"Не удалось извлечь tg_id из Heleket webhook: {data}")
|
||||
logger.error(
|
||||
f"Не удалось извлечь tg_id из Heleket webhook: {data}"
|
||||
)
|
||||
return False
|
||||
balance_amount = rub_amount if rub_amount else float(merchant_amount)
|
||||
balance_amount = (
|
||||
rub_amount if rub_amount else float(merchant_amount)
|
||||
)
|
||||
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"Heleket: платёж {order_id} уже обработан")
|
||||
logger.info(
|
||||
f"Heleket: платёж {order_id} уже обработан"
|
||||
)
|
||||
return True
|
||||
ok = await update_payment_status(session=session, internal_id=int(payment["id"]), new_status="success")
|
||||
ok = await update_payment_status(
|
||||
session=session,
|
||||
internal_id=int(payment["id"]),
|
||||
new_status="success",
|
||||
)
|
||||
if not ok:
|
||||
logger.error(f"Heleket: не удалось обновить статус платежа {order_id}")
|
||||
logger.error(
|
||||
f"Heleket: не удалось обновить статус "
|
||||
f"платежа {order_id}"
|
||||
)
|
||||
return False
|
||||
await update_balance(session, tg_id, balance_amount)
|
||||
await send_payment_success_notification(tg_id, balance_amount, session)
|
||||
await send_payment_success_notification(
|
||||
tg_id, balance_amount, session
|
||||
)
|
||||
await session.commit()
|
||||
else:
|
||||
await add_payment(
|
||||
@@ -103,37 +156,51 @@ async def process_heleket_webhook(data: dict) -> bool:
|
||||
metadata=None,
|
||||
)
|
||||
await update_balance(session, tg_id, balance_amount)
|
||||
await send_payment_success_notification(tg_id, balance_amount, session)
|
||||
await send_payment_success_notification(
|
||||
tg_id, balance_amount, session
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(f"Heleket: платёж {order_id} для пользователя {tg_id} успешно обработан, баланс пополнен на {balance_amount} RUB")
|
||||
logger.info(
|
||||
f"Heleket: платёж {order_id} для пользователя {tg_id} "
|
||||
f"успешно обработан, баланс пополнен на {balance_amount} RUB"
|
||||
)
|
||||
return True
|
||||
elif status in ['fail', 'wrong_amount', 'cancel', 'system_fail']:
|
||||
logger.warning(f"Heleket: неудачный платёж {order_id}, статус: {status}")
|
||||
|
||||
elif status in ["fail", "wrong_amount", "cancel", "system_fail"]:
|
||||
logger.warning(
|
||||
f"Heleket: неудачный платёж {order_id}, статус: {status}"
|
||||
)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
payment = await get_payment_by_payment_id(session, order_id)
|
||||
if payment:
|
||||
await update_payment_status(session=session, internal_id=int(payment["id"]), new_status="failed")
|
||||
await update_payment_status(
|
||||
session=session,
|
||||
internal_id=int(payment["id"]),
|
||||
new_status="failed",
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
else:
|
||||
logger.info(f"Heleket: промежуточный статус {status} для платежа {order_id}")
|
||||
return True
|
||||
logger.info(
|
||||
f"Heleket: промежуточный статус {status} "
|
||||
f"для платежа {order_id}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки Heleket webhook: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def heleket_webhook(request: web.Request):
|
||||
"""Обработчик вебхука Heleket для aiohttp"""
|
||||
"""Обработчик webhook от Heleket для aiohttp."""
|
||||
try:
|
||||
data = await request.json()
|
||||
logger.info(f"Heleket webhook received from {request.remote}")
|
||||
|
||||
|
||||
if not verify_heleket_signature(data):
|
||||
logger.error("Heleket webhook: неверная подпись")
|
||||
return web.Response(status=400, text="Invalid signature")
|
||||
|
||||
|
||||
success = await process_heleket_webhook(data)
|
||||
if success:
|
||||
return web.Response(status=200, text="OK")
|
||||
|
||||
@@ -1,30 +1,182 @@
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from handlers.buttons import PAY_2, MAIN_MENU
|
||||
from database import get_temporary_data
|
||||
from database.models import User
|
||||
from handlers.buttons import MAIN_MENU, PAY_2
|
||||
from handlers.payments.currency_rates import format_for_user
|
||||
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
|
||||
from .service import (
|
||||
KASSAI_PAYMENT_METHODS,
|
||||
generate_kassai_payment_link,
|
||||
process_callback_pay_kassai,
|
||||
router as service_router,
|
||||
)
|
||||
|
||||
router = Router(name="kassai_router")
|
||||
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")
|
||||
async def handle_pay_kassai_cards(
|
||||
callback_query: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
):
|
||||
"""Обработчик оплаты через KassaI картами."""
|
||||
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_pay_kassai_sbp(
|
||||
callback_query: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
):
|
||||
"""Обработчик оплаты через KassaI СБП."""
|
||||
await process_callback_pay_kassai(
|
||||
callback_query, state, session, method_name="sbp"
|
||||
)
|
||||
|
||||
|
||||
async def _handle_custom_amount_input_kassai(
|
||||
event,
|
||||
session: AsyncSession,
|
||||
method_name: str,
|
||||
pay_button_text: str = PAY_2,
|
||||
main_menu_text: str = MAIN_MENU,
|
||||
):
|
||||
"""Универсальная функция быстрого потока для KassaI.
|
||||
|
||||
Принимает недостающую сумму и формирует платеж.
|
||||
Работает с временными данными из fast_payment_flow для
|
||||
создания/продления/подарка.
|
||||
|
||||
Args:
|
||||
event: Событие от aiogram
|
||||
session: Сессия БД
|
||||
method_name: "cards" или "sbp"
|
||||
pay_button_text: Текст кнопки оплаты
|
||||
main_menu_text: Текст кнопки главного меню
|
||||
"""
|
||||
message = event.message
|
||||
from_user = event.from_user
|
||||
tg_id = from_user.id
|
||||
|
||||
temp_data = await get_temporary_data(session, tg_id)
|
||||
valid_states = [
|
||||
"waiting_for_payment",
|
||||
"waiting_for_renewal_payment",
|
||||
"waiting_for_gift_payment",
|
||||
]
|
||||
if not temp_data or temp_data["state"] not in valid_states:
|
||||
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
|
||||
|
||||
min_amounts = {"cards": 50, "sbp": 10}
|
||||
method_labels = {"cards": "картой", "sbp": "через СБП"}
|
||||
min_amount = min_amounts.get(method_name, 10)
|
||||
|
||||
if amount < min_amount:
|
||||
method_label = method_labels.get(method_name, "")
|
||||
error_msg = (
|
||||
f"❌ Минимальная сумма для оплаты {method_label} — "
|
||||
f"{min_amount}₽."
|
||||
)
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text=error_msg,
|
||||
)
|
||||
return
|
||||
|
||||
method = next(
|
||||
(
|
||||
m
|
||||
for m in KASSAI_PAYMENT_METHODS
|
||||
if m["name"] == method_name and m["enable"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not method:
|
||||
method_label = "картами" if method_name == "cards" else "через СБП"
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text=f"❌ Оплата {method_label} KassaAI временно недоступна.",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
payment_url = await generate_kassai_payment_link(
|
||||
amount, tg_id, method
|
||||
)
|
||||
|
||||
if not payment_url or payment_url == "https://fk.life/":
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text=(
|
||||
"❌ Произошла ошибка при создании платежа. "
|
||||
"Попробуйте позже или выберите другой способ оплаты."
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
markup = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=main_menu_text, callback_data="profile"
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(User.language_code).where(User.tg_id == tg_id)
|
||||
)
|
||||
language_code = result.scalar_one_or_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:
|
||||
method_label = "Cards" if method_name == "cards" else "SBP"
|
||||
logger.error(
|
||||
f"Ошибка при создании платежа KassaAI {method_label} "
|
||||
f"для пользователя {tg_id}: {e}"
|
||||
)
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="Произошла ошибка при создании платежа. Попробуйте позже.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
|
||||
)
|
||||
|
||||
|
||||
async def handle_custom_amount_input_kassai_cards(
|
||||
@@ -33,65 +185,10 @@ async def handle_custom_amount_input_kassai_cards(
|
||||
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
|
||||
|
||||
if amount < 50:
|
||||
await edit_or_send_message(target_message=message, text="❌ Минимальная сумма для оплаты картой — 50₽.")
|
||||
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)
|
||||
|
||||
if not payment_url or payment_url == "https://fk.life/":
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="❌ Произошла ошибка при создании платежа. Попробуйте позже или выберите другой способ оплаты.",
|
||||
)
|
||||
return
|
||||
|
||||
markup = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
|
||||
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
|
||||
]
|
||||
)
|
||||
|
||||
from database.models import User
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User.language_code).where(User.tg_id == tg_id))
|
||||
language_code = result.scalar_one_or_none()
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code)
|
||||
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=[]),
|
||||
)
|
||||
"""Функция быстрого потока для KassaI Cards."""
|
||||
await _handle_custom_amount_input_kassai(
|
||||
event, session, "cards", pay_button_text, main_menu_text
|
||||
)
|
||||
|
||||
|
||||
async def handle_custom_amount_input_kassai_sbp(
|
||||
@@ -100,62 +197,8 @@ async def handle_custom_amount_input_kassai_sbp(
|
||||
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
|
||||
"""Функция быстрого потока для KassaI SBP."""
|
||||
await _handle_custom_amount_input_kassai(
|
||||
event, session, "sbp", pay_button_text, main_menu_text
|
||||
)
|
||||
|
||||
try:
|
||||
payment_url = await generate_kassai_payment_link(amount, tg_id, method)
|
||||
|
||||
if not payment_url or payment_url == "https://fk.life/":
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text="❌ Произошла ошибка при создании платежа. Попробуйте позже или выберите другой способ оплаты.",
|
||||
)
|
||||
return
|
||||
|
||||
markup = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=pay_button_text, url=payment_url)],
|
||||
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
|
||||
]
|
||||
)
|
||||
|
||||
from database.models import User
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User.language_code).where(User.tg_id == tg_id))
|
||||
language_code = result.scalar_one_or_none()
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code)
|
||||
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=[]),
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
import aiohttp
|
||||
|
||||
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 import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from handlers.payments.currency_rates import format_for_user
|
||||
|
||||
from config import (
|
||||
KASSAI_API_KEY,
|
||||
@@ -20,25 +20,34 @@ from config import (
|
||||
KASSAI_SUCCESS_URL,
|
||||
PROVIDERS_ENABLED,
|
||||
)
|
||||
from handlers.payments.providers import get_providers
|
||||
from database.models import User
|
||||
from handlers.buttons import BACK, KASSAI_CARDS, KASSAI_SBP, PAY_2
|
||||
from handlers.payments.currency_rates import (
|
||||
format_for_user,
|
||||
pick_currency,
|
||||
to_rub,
|
||||
)
|
||||
from handlers.payments.keyboards import (
|
||||
build_amounts_keyboard,
|
||||
parse_amount_from_callback,
|
||||
pay_keyboard,
|
||||
payment_options_for_user,
|
||||
)
|
||||
from handlers.payments.providers import get_providers
|
||||
from handlers.texts import (
|
||||
ENTER_SUM,
|
||||
KASSAI_CARDS_DESCRIPTION,
|
||||
KASSAI_PAYMENT_MESSAGE,
|
||||
KASSAI_SBP_DESCRIPTION,
|
||||
)
|
||||
from handlers.payments.keyboards import build_amounts_keyboard, payment_options_for_user, parse_amount_from_callback, pay_keyboard
|
||||
from handlers.utils import edit_or_send_message
|
||||
from database.models import User
|
||||
from logger import logger
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
async def get_user_language(session: AsyncSession, tg_id: int) -> str | None:
|
||||
"""Получает язык пользователя из базы данных"""
|
||||
from sqlalchemy import select
|
||||
"""Получает язык пользователя из базы данных."""
|
||||
result = await session.execute(
|
||||
select(User.language_code).where(User.tg_id == tg_id)
|
||||
)
|
||||
@@ -46,6 +55,8 @@ async def get_user_language(session: AsyncSession, tg_id: int) -> str | None:
|
||||
|
||||
|
||||
class ReplenishBalanceKassaiState(StatesGroup):
|
||||
"""Состояния FSM для пополнения баланса через KassaI."""
|
||||
|
||||
choosing_method = State()
|
||||
choosing_amount = State()
|
||||
waiting_for_payment_confirmation = State()
|
||||
@@ -71,18 +82,28 @@ KASSAI_PAYMENT_METHODS = [
|
||||
]
|
||||
|
||||
|
||||
|
||||
@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
|
||||
callback_query: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
session: AsyncSession,
|
||||
method_name: str = None,
|
||||
):
|
||||
"""Обработчик callback для инициализации платежа через KassaI."""
|
||||
try:
|
||||
tg_id = callback_query.from_user.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)
|
||||
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()
|
||||
@@ -95,13 +116,15 @@ async def process_callback_pay_kassai(
|
||||
return
|
||||
|
||||
language_code = await get_user_language(session, tg_id)
|
||||
opts = await payment_options_for_user(session, tg_id, language_code)
|
||||
opts = await payment_options_for_user(
|
||||
session, tg_id, language_code, force_currency="RUB"
|
||||
)
|
||||
builder = build_amounts_keyboard(
|
||||
prefix=f"kassai_{method_name}",
|
||||
pattern="{prefix}_amount|{price}",
|
||||
back_cb="balance",
|
||||
custom_cb=f"kassai_custom_amount|{method_name}",
|
||||
opts=opts
|
||||
opts=opts,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -123,7 +146,12 @@ async def process_callback_pay_kassai(
|
||||
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=method["button"],
|
||||
callback_data=f"kassai_method|{method['name']}",
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
|
||||
|
||||
try:
|
||||
@@ -134,12 +162,20 @@ async def process_callback_pay_kassai(
|
||||
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.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)
|
||||
logger.error(
|
||||
f"Error in process_callback_pay_kassai for user "
|
||||
f"{callback_query.message.chat.id}: {e}"
|
||||
)
|
||||
await callback_query.answer(
|
||||
"Произошла ошибка при инициализации платежа. Попробуйте позже.",
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("kassai_method|"))
|
||||
@@ -160,7 +196,9 @@ async def process_method_selection(callback_query: types.CallbackQuery, state: F
|
||||
tg_id = callback_query.from_user.id
|
||||
|
||||
language_code = await get_user_language(session, tg_id)
|
||||
opts = await payment_options_for_user(session, tg_id, language_code)
|
||||
opts = await payment_options_for_user(
|
||||
session, tg_id, language_code, force_currency="RUB"
|
||||
)
|
||||
builder = build_amounts_keyboard(
|
||||
prefix=f"kassai_{method_name}",
|
||||
pattern="{prefix}_amount|{price}",
|
||||
@@ -187,7 +225,6 @@ async def process_custom_amount_button(callback_query: types.CallbackQuery, stat
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_kassai_{method_name}"))
|
||||
|
||||
from handlers.payments.currency_rates import pick_currency
|
||||
language_code = await get_user_language(session, callback_query.from_user.id)
|
||||
currency = pick_currency(language_code)
|
||||
|
||||
@@ -216,7 +253,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext,
|
||||
)
|
||||
return
|
||||
|
||||
from handlers.payments.currency_rates import pick_currency
|
||||
language_code = await get_user_language(session, message.from_user.id)
|
||||
currency = pick_currency(language_code)
|
||||
|
||||
@@ -256,7 +292,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext,
|
||||
)
|
||||
return
|
||||
|
||||
from handlers.payments.currency_rates import to_rub, format_for_user
|
||||
if currency == "RUB":
|
||||
amount_rub = user_amount
|
||||
else:
|
||||
@@ -278,7 +313,7 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext,
|
||||
confirm_keyboard = pay_keyboard(payment_url, pay_text=PAY_2, back_cb="balance")
|
||||
|
||||
tg_id = message.from_user.id
|
||||
amount_text = await format_for_user(session, tg_id, float(amount_rub), language_code)
|
||||
amount_text = await format_for_user(session, tg_id, float(amount_rub), language_code, force_currency="RUB")
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
@@ -347,7 +382,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
|
||||
|
||||
tg_id = callback_query.from_user.id
|
||||
language_code = await get_user_language(session, tg_id)
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code)
|
||||
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="RUB")
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
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 config import KASSAI_SECRET_KEY, KASSAI_SHOP_ID
|
||||
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
|
||||
from logger import logger
|
||||
|
||||
|
||||
def verify_kassai_signature(data: dict, signature: str) -> bool:
|
||||
"""Проверяет подпись webhook от KassaAI.
|
||||
|
||||
Args:
|
||||
data: Данные от webhook
|
||||
signature: Подпись для проверки
|
||||
|
||||
Returns:
|
||||
True если подпись валидна, иначе False
|
||||
"""
|
||||
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()
|
||||
sign_string = (
|
||||
f"{KASSAI_SHOP_ID}:{data.get('AMOUNT', '')}:"
|
||||
f"{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"KassaAI signature mismatch. "
|
||||
f"Expected: {expected_signature}, Got: {signature}"
|
||||
)
|
||||
logger.error(f"Sign string: {sign_string}")
|
||||
else:
|
||||
logger.info("KassaAI webhook: подпись успешно проверена")
|
||||
@@ -23,46 +48,66 @@ def verify_kassai_signature(data: dict, signature: str) -> bool:
|
||||
|
||||
|
||||
async def kassai_webhook(request: web.Request):
|
||||
"""Обработчик webhook от KassaAI для aiohttp."""
|
||||
try:
|
||||
data = await request.post()
|
||||
data = await request.post()
|
||||
logger.info(f"KassaAI webhook received: {dict(data)}")
|
||||
signature = data.get('SIGN', '')
|
||||
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')
|
||||
|
||||
|
||||
amount_raw = data.get("AMOUNT")
|
||||
order_id = data.get("MERCHANT_ORDER_ID")
|
||||
|
||||
if not amount_raw or not order_id:
|
||||
logger.error("KassaAI webhook: отсутствуют обязательные параметры")
|
||||
logger.error(
|
||||
"KassaAI webhook: отсутствуют обязательные параметры"
|
||||
)
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
amount = float(amount_raw)
|
||||
|
||||
|
||||
try:
|
||||
tg_id = int(order_id.split('_')[1])
|
||||
tg_id = int(order_id.split("_")[1])
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.error(f"KassaAI webhook: не удалось извлечь tg_id из order_id {order_id}: {e}")
|
||||
logger.error(
|
||||
f"KassaAI webhook: не удалось извлечь tg_id "
|
||||
f"из order_id {order_id}: {e}"
|
||||
)
|
||||
return web.Response(status=400)
|
||||
|
||||
logger.info(f"KassaAI: успешный платёж {order_id} на сумму {amount} RUB для пользователя {tg_id}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"KassaAI: успешный платёж {order_id} на сумму {amount} RUB "
|
||||
f"для пользователя {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} уже обработан")
|
||||
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")
|
||||
ok = await update_payment_status(
|
||||
session=session,
|
||||
internal_id=int(payment["id"]),
|
||||
new_status="success",
|
||||
)
|
||||
if not ok:
|
||||
logger.error(f"KassaAI: не удалось обновить статус платежа {order_id}")
|
||||
logger.error(
|
||||
f"KassaAI: не удалось обновить статус "
|
||||
f"платежа {order_id}"
|
||||
)
|
||||
return web.Response(status=500)
|
||||
await update_balance(session, tg_id, amount)
|
||||
await send_payment_success_notification(tg_id, amount, session)
|
||||
await send_payment_success_notification(
|
||||
tg_id, amount, session
|
||||
)
|
||||
await session.commit()
|
||||
else:
|
||||
await add_payment(
|
||||
@@ -76,9 +121,14 @@ async def kassai_webhook(request: web.Request):
|
||||
metadata=None,
|
||||
)
|
||||
await update_balance(session, tg_id, amount)
|
||||
await send_payment_success_notification(tg_id, amount, session)
|
||||
await send_payment_success_notification(
|
||||
tg_id, amount, session
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(f"KassaAI: платёж {order_id} успешно обработан, баланс пользователя {tg_id} пополнен на {amount} RUB")
|
||||
logger.info(
|
||||
f"KassaAI: платёж {order_id} успешно обработан, "
|
||||
f"баланс пользователя {tg_id} пополнен на {amount} RUB"
|
||||
)
|
||||
return web.Response(text="OK")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки KassaAI webhook: {e}")
|
||||
|
||||
Reference in New Issue
Block a user