FIX "Консистентный дизайн касс"

- FIX "Консистентный дизайн касс" Heleket и Kassai
- Оптимизация касс по PEP8
- Оптимизация функций KassaI
This commit is contained in:
Boris Kovalskii
2025-11-16 12:00:19 +10:00
committed by GitHub
parent 8b77ccda9e
commit 9f353e8b5c
6 changed files with 537 additions and 276 deletions
+83 -30
View File
@@ -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=[]),
)
+30 -17
View File
@@ -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,
+121 -54
View File
@@ -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")
+170 -127
View File
@@ -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=[]),
)
+57 -22
View File
@@ -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,
+76 -26
View File
@@ -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}")