Fixed: back button in stars/ traffic display/ trial in country mode. Added error duplicate handling and request queueing.
This commit is contained in:
@@ -250,6 +250,38 @@ async def try_fast_payment_flow(
|
||||
return True
|
||||
|
||||
|
||||
@router.callback_query(F.data == "fastflow_back")
|
||||
async def fastflow_back(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
||||
"""Возврат из экрана выбора суммы в потоке /buy к выбору способа оплаты (без перехода на экран баланса)."""
|
||||
amount_not_found_text = "Сумма не найдена"
|
||||
data = await state.get_data()
|
||||
temp_key = data.get("temp_key")
|
||||
temp_payload = data.get("temp_payload")
|
||||
required_amount = data.get("required_amount")
|
||||
|
||||
if not temp_key or not isinstance(temp_payload, dict) or required_amount is None:
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=amount_not_found_text,
|
||||
reply_markup=InlineKeyboardBuilder()
|
||||
.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
|
||||
.as_markup(),
|
||||
)
|
||||
await callback_query.answer()
|
||||
return
|
||||
|
||||
await try_fast_payment_flow(
|
||||
callback_query,
|
||||
session,
|
||||
state,
|
||||
tg_id=callback_query.from_user.id,
|
||||
temp_key=str(temp_key),
|
||||
temp_payload=dict(temp_payload),
|
||||
required_amount=int(required_amount),
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "fastflow_coupon_back")
|
||||
async def fastflow_coupon_back(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
||||
amount_not_found_text = "Сумма не найдена"
|
||||
|
||||
@@ -1,353 +1,352 @@
|
||||
import hashlib
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
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 aiohttp import web
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import (
|
||||
FREEKASSA_SECRET1,
|
||||
FREEKASSA_SECRET2,
|
||||
FREEKASSA_SHOP_ID,
|
||||
)
|
||||
from database import (
|
||||
add_payment,
|
||||
add_user,
|
||||
async_session_maker,
|
||||
check_user_exists,
|
||||
clear_temporary_data,
|
||||
get_key_count,
|
||||
get_payment_by_payment_id,
|
||||
get_temporary_data,
|
||||
update_balance,
|
||||
)
|
||||
from handlers.buttons import BACK, PAY_2
|
||||
from handlers.payments.utils import send_payment_success_notification
|
||||
from handlers.texts import DEFAULT_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS
|
||||
from handlers.utils import edit_or_send_message
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class ReplenishBalanceState(StatesGroup):
|
||||
choosing_amount_freekassa = State()
|
||||
waiting_for_payment_confirmation_freekassa = State()
|
||||
|
||||
|
||||
def generate_signature(shop_id: int, amount: float, secret: str, order_id: str, currency: str = "RUB") -> str:
|
||||
signature_string = f"{shop_id}:{amount}:{secret}:{currency}:{order_id}"
|
||||
signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest()
|
||||
logger.debug(f"Generated signature for order {order_id}: {signature}")
|
||||
return signature
|
||||
|
||||
|
||||
def generate_payment_link(amount: float, order_id: str, tg_id: int, currency: str = "RUB") -> str:
|
||||
signature = generate_signature(FREEKASSA_SHOP_ID, amount, FREEKASSA_SECRET1, order_id, currency)
|
||||
|
||||
payment_url = "https://pay.fk.money/"
|
||||
params = {
|
||||
"m": FREEKASSA_SHOP_ID,
|
||||
"oa": amount,
|
||||
"currency": currency,
|
||||
"o": order_id,
|
||||
"s": signature,
|
||||
"us_tg_id": tg_id,
|
||||
}
|
||||
|
||||
query_string = "&".join([f"{key}={value}" for key, value in params.items()])
|
||||
full_url = f"{payment_url}?{query_string}"
|
||||
|
||||
logger.info(f"Generated Freekassa payment link: {full_url}")
|
||||
return full_url
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pay_freekassa")
|
||||
async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
|
||||
tg_id = callback_query.message.chat.id
|
||||
logger.info(f"User {tg_id} initiated Freekassa payment.")
|
||||
|
||||
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"freekassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i + 1]["text"],
|
||||
callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i]["text"],
|
||||
callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
|
||||
|
||||
key_count = await get_key_count(session, tg_id)
|
||||
|
||||
if key_count == 0:
|
||||
exists = await check_user_exists(session, tg_id)
|
||||
if not exists:
|
||||
from_user = callback_query.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
logger.info(f"[DB] Новый пользователь {tg_id} создан через Freekassa.")
|
||||
|
||||
await callback_query.message.delete()
|
||||
|
||||
new_message = await callback_query.message.answer(
|
||||
text="Выберите сумму пополнения:",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id)
|
||||
await state.set_state(ReplenishBalanceState.choosing_amount_freekassa)
|
||||
logger.info(f"Displayed amount selection for user {tg_id}.")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("freekassa_amount|"))
|
||||
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
logger.info(f"Получены данные callback_data: {callback_query.data}")
|
||||
|
||||
data = callback_query.data.split("|")
|
||||
if len(data) != 3 or data[1] != "amount":
|
||||
logger.error("Ошибка: callback_data не соответствует формату.")
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="Ошибка: данные повреждены.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
amount_str = data[2]
|
||||
try:
|
||||
amount = float(amount_str)
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительным числом.")
|
||||
except ValueError as e:
|
||||
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="Некорректная сумма.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(amount=amount)
|
||||
logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.")
|
||||
|
||||
tg_id = callback_query.message.chat.id
|
||||
order_id = f"order_{tg_id}_{int(amount)}_{hash(str(tg_id) + str(amount))}"
|
||||
|
||||
payment_url = generate_payment_link(amount, order_id, tg_id)
|
||||
|
||||
logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}")
|
||||
|
||||
confirm_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
|
||||
[InlineKeyboardButton(text=BACK, callback_data="pay_freekassa")],
|
||||
]
|
||||
)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=DEFAULT_PAYMENT_MESSAGE.format(amount=amount),
|
||||
reply_markup=confirm_keyboard,
|
||||
)
|
||||
logger.info(f"Payment link sent to user {callback_query.message.chat.id}.")
|
||||
|
||||
|
||||
def verify_signature(params: dict) -> bool:
|
||||
try:
|
||||
merchant_id = params.get("MERCHANT_ID", "")
|
||||
amount = params.get("AMOUNT", "")
|
||||
merchant_order_id = params.get("MERCHANT_ORDER_ID", "")
|
||||
sign = params.get("SIGN", "")
|
||||
|
||||
signature_string = f"{merchant_id}:{amount}:{FREEKASSA_SECRET2}:{merchant_order_id}"
|
||||
expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest()
|
||||
|
||||
logger.debug(f"Signature verification: expected={expected_signature}, received={sign}")
|
||||
|
||||
return expected_signature == sign
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying signature: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def freekassa_webhook(request: web.Request):
|
||||
try:
|
||||
params = dict(request.query)
|
||||
logger.info(f"Received Freekassa webhook: {params}")
|
||||
|
||||
merchant_id = params.get("MERCHANT_ID")
|
||||
amount = params.get("AMOUNT")
|
||||
merchant_order_id = params.get("MERCHANT_ORDER_ID")
|
||||
sign = params.get("SIGN")
|
||||
tg_id = params.get("us_tg_id")
|
||||
|
||||
if not all([merchant_id, amount, merchant_order_id, sign]):
|
||||
logger.error("Missing required parameters in webhook")
|
||||
return web.Response(status=400, text="Missing required parameters")
|
||||
|
||||
if not verify_signature(params):
|
||||
logger.error("Invalid signature in webhook")
|
||||
return web.Response(status=400, text="Invalid signature")
|
||||
|
||||
if str(merchant_id) != str(FREEKASSA_SHOP_ID):
|
||||
logger.error(f"Invalid merchant_id: {merchant_id}")
|
||||
return web.Response(status=400, text="Invalid merchant_id")
|
||||
|
||||
try:
|
||||
amount_float = float(amount)
|
||||
if tg_id:
|
||||
tg_id_int = int(tg_id)
|
||||
else:
|
||||
order_parts = merchant_order_id.split("_")
|
||||
if len(order_parts) >= 3 and order_parts[0] == "order":
|
||||
tg_id_int = int(order_parts[1])
|
||||
else:
|
||||
logger.error(f"Cannot extract tg_id from order_id: {merchant_order_id}")
|
||||
return web.Response(status=400, text="Cannot identify user")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"Error parsing parameters: {e}")
|
||||
return web.Response(status=400, text="Invalid parameter format")
|
||||
|
||||
async with async_session_maker() as session:
|
||||
|
||||
existing = await get_payment_by_payment_id(session, merchant_order_id)
|
||||
if existing and existing.get("status") == "success":
|
||||
logger.warning(
|
||||
f"[Freekassa] Повторный webhook. Платёж уже обработан: order_id={merchant_order_id}"
|
||||
)
|
||||
return web.Response(text="YES")
|
||||
|
||||
await update_balance(session, tg_id_int, amount_float)
|
||||
await send_payment_success_notification(tg_id_int, amount_float, session)
|
||||
await add_payment(
|
||||
session, tg_id_int, amount_float, "freekassa", payment_id=merchant_order_id
|
||||
)
|
||||
await clear_temporary_data(session, tg_id_int)
|
||||
|
||||
logger.info(f"Payment processed successfully. User: {tg_id_int}, Amount: {amount_float}")
|
||||
return web.Response(text="YES")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Freekassa webhook: {e}")
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "enter_custom_amount_freekassa")
|
||||
async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
tg_id = callback_query.message.chat.id
|
||||
logger.info(f"User {tg_id} chose to enter a custom amount.")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_freekassa"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=ENTER_SUM,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_freekassa)
|
||||
|
||||
|
||||
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_freekassa)
|
||||
async def handle_custom_amount_input(
|
||||
message: types.Message | types.CallbackQuery,
|
||||
state: FSMContext = None,
|
||||
session: AsyncSession = None,
|
||||
):
|
||||
if isinstance(message, types.CallbackQuery):
|
||||
tg_id = message.message.chat.id
|
||||
target_message = message.message
|
||||
else:
|
||||
tg_id = message.chat.id
|
||||
target_message = message
|
||||
|
||||
logger.info(f"User {tg_id} initiated payment through Freekassa")
|
||||
|
||||
try:
|
||||
user_data = await get_temporary_data(session, tg_id)
|
||||
|
||||
if not user_data:
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Данные для оплаты не найдены. Попробуйте снова.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
state_type = user_data["state"]
|
||||
amount = user_data["data"].get("required_amount", 0)
|
||||
|
||||
if amount <= 0:
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Недостаточная сумма для пополнения.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
order_id = f"order_{tg_id}_{int(amount)}_{hash(str(tg_id) + str(amount))}"
|
||||
payment_url = generate_payment_link(amount, order_id, tg_id)
|
||||
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text="💳 Оплатить", url=payment_url))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_freekassa"))
|
||||
|
||||
if state_type == "waiting_for_payment":
|
||||
message_text = (
|
||||
f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
|
||||
)
|
||||
elif state_type == "waiting_for_renewal_payment":
|
||||
message_text = (
|
||||
f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
|
||||
)
|
||||
else:
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Некорректное состояние данных. Попробуйте снова.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text=message_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
if isinstance(state, FSMContext):
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Произошла ошибка при создании платежа. Попробуйте позже.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
import hashlib
|
||||
|
||||
from typing import Any
|
||||
|
||||
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 aiohttp import web
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import (
|
||||
FREEKASSA_SECRET1,
|
||||
FREEKASSA_SECRET2,
|
||||
FREEKASSA_SHOP_ID,
|
||||
)
|
||||
from database import (
|
||||
add_payment,
|
||||
add_user,
|
||||
async_session_maker,
|
||||
check_user_exists,
|
||||
clear_temporary_data,
|
||||
get_key_count,
|
||||
get_payment_by_payment_id,
|
||||
get_temporary_data,
|
||||
update_balance,
|
||||
)
|
||||
from handlers.buttons import BACK, PAY_2
|
||||
from handlers.payments.utils import send_payment_success_notification
|
||||
from handlers.texts import DEFAULT_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS
|
||||
from handlers.utils import edit_or_send_message
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class ReplenishBalanceState(StatesGroup):
|
||||
choosing_amount_freekassa = State()
|
||||
waiting_for_payment_confirmation_freekassa = State()
|
||||
|
||||
|
||||
def generate_signature(shop_id: int, amount: float, secret: str, order_id: str, currency: str = "RUB") -> str:
|
||||
signature_string = f"{shop_id}:{amount}:{secret}:{currency}:{order_id}"
|
||||
signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest()
|
||||
logger.debug(f"Generated signature for order {order_id}: {signature}")
|
||||
return signature
|
||||
|
||||
|
||||
def generate_payment_link(amount: float, order_id: str, tg_id: int, currency: str = "RUB") -> str:
|
||||
signature = generate_signature(FREEKASSA_SHOP_ID, amount, FREEKASSA_SECRET1, order_id, currency)
|
||||
|
||||
payment_url = "https://pay.fk.money/"
|
||||
params = {
|
||||
"m": FREEKASSA_SHOP_ID,
|
||||
"oa": amount,
|
||||
"currency": currency,
|
||||
"o": order_id,
|
||||
"s": signature,
|
||||
"us_tg_id": tg_id,
|
||||
}
|
||||
|
||||
query_string = "&".join([f"{key}={value}" for key, value in params.items()])
|
||||
full_url = f"{payment_url}?{query_string}"
|
||||
|
||||
logger.info(f"Generated Freekassa payment link: {full_url}")
|
||||
return full_url
|
||||
|
||||
|
||||
@router.callback_query(F.data == "pay_freekassa")
|
||||
async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
|
||||
tg_id = callback_query.message.chat.id
|
||||
logger.info(f"User {tg_id} initiated Freekassa payment.")
|
||||
|
||||
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"freekassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i + 1]["text"],
|
||||
callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i]["text"],
|
||||
callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
|
||||
|
||||
key_count = await get_key_count(session, tg_id)
|
||||
|
||||
if key_count == 0:
|
||||
exists = await check_user_exists(session, tg_id)
|
||||
if not exists:
|
||||
from_user = callback_query.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
logger.info(f"[DB] Новый пользователь {tg_id} создан через Freekassa.")
|
||||
|
||||
await callback_query.message.delete()
|
||||
|
||||
new_message = await callback_query.message.answer(
|
||||
text="Выберите сумму пополнения:",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id)
|
||||
await state.set_state(ReplenishBalanceState.choosing_amount_freekassa)
|
||||
logger.info(f"Displayed amount selection for user {tg_id}.")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("freekassa_amount|"))
|
||||
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
logger.info(f"Получены данные callback_data: {callback_query.data}")
|
||||
|
||||
data = callback_query.data.split("|")
|
||||
if len(data) != 3 or data[1] != "amount":
|
||||
logger.error("Ошибка: callback_data не соответствует формату.")
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="Ошибка: данные повреждены.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
amount_str = data[2]
|
||||
try:
|
||||
amount = float(amount_str)
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительным числом.")
|
||||
except ValueError as e:
|
||||
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="Некорректная сумма.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(amount=amount)
|
||||
logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.")
|
||||
|
||||
tg_id = callback_query.message.chat.id
|
||||
order_id = f"order_{tg_id}_{int(amount)}_{hash(str(tg_id) + str(amount))}"
|
||||
|
||||
payment_url = generate_payment_link(amount, order_id, tg_id)
|
||||
|
||||
logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}")
|
||||
|
||||
confirm_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
|
||||
[InlineKeyboardButton(text=BACK, callback_data="pay_freekassa")],
|
||||
]
|
||||
)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=DEFAULT_PAYMENT_MESSAGE.format(amount=amount),
|
||||
reply_markup=confirm_keyboard,
|
||||
)
|
||||
logger.info(f"Payment link sent to user {callback_query.message.chat.id}.")
|
||||
|
||||
|
||||
def verify_signature(params: dict) -> bool:
|
||||
try:
|
||||
merchant_id = params.get("MERCHANT_ID", "")
|
||||
amount = params.get("AMOUNT", "")
|
||||
merchant_order_id = params.get("MERCHANT_ORDER_ID", "")
|
||||
sign = params.get("SIGN", "")
|
||||
|
||||
signature_string = f"{merchant_id}:{amount}:{FREEKASSA_SECRET2}:{merchant_order_id}"
|
||||
expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest()
|
||||
|
||||
logger.debug(f"Signature verification: expected={expected_signature}, received={sign}")
|
||||
|
||||
return expected_signature == sign
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying signature: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def freekassa_webhook(request: web.Request):
|
||||
try:
|
||||
params = dict(request.query)
|
||||
logger.info(f"Received Freekassa webhook: {params}")
|
||||
|
||||
merchant_id = params.get("MERCHANT_ID")
|
||||
amount = params.get("AMOUNT")
|
||||
merchant_order_id = params.get("MERCHANT_ORDER_ID")
|
||||
sign = params.get("SIGN")
|
||||
tg_id = params.get("us_tg_id")
|
||||
|
||||
if not all([merchant_id, amount, merchant_order_id, sign]):
|
||||
logger.error("Missing required parameters in webhook")
|
||||
return web.Response(status=400, text="Missing required parameters")
|
||||
|
||||
if not verify_signature(params):
|
||||
logger.error("Invalid signature in webhook")
|
||||
return web.Response(status=400, text="Invalid signature")
|
||||
|
||||
if str(merchant_id) != str(FREEKASSA_SHOP_ID):
|
||||
logger.error(f"Invalid merchant_id: {merchant_id}")
|
||||
return web.Response(status=400, text="Invalid merchant_id")
|
||||
|
||||
try:
|
||||
amount_float = float(amount)
|
||||
if tg_id:
|
||||
tg_id_int = int(tg_id)
|
||||
else:
|
||||
order_parts = merchant_order_id.split("_")
|
||||
if len(order_parts) >= 3 and order_parts[0] == "order":
|
||||
tg_id_int = int(order_parts[1])
|
||||
else:
|
||||
logger.error(f"Cannot extract tg_id from order_id: {merchant_order_id}")
|
||||
return web.Response(status=400, text="Cannot identify user")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"Error parsing parameters: {e}")
|
||||
return web.Response(status=400, text="Invalid parameter format")
|
||||
|
||||
async with async_session_maker() as session:
|
||||
|
||||
existing = await get_payment_by_payment_id(session, merchant_order_id)
|
||||
if existing and existing.get("status") == "success":
|
||||
logger.warning(
|
||||
f"[Freekassa] Повторный webhook. Платёж уже обработан: order_id={merchant_order_id}"
|
||||
)
|
||||
return web.Response(text="YES")
|
||||
|
||||
await update_balance(session, tg_id_int, amount_float)
|
||||
await send_payment_success_notification(tg_id_int, amount_float, session)
|
||||
await add_payment(
|
||||
session, tg_id_int, amount_float, "freekassa", payment_id=merchant_order_id
|
||||
)
|
||||
await clear_temporary_data(session, tg_id_int)
|
||||
|
||||
logger.info(f"Payment processed successfully. User: {tg_id_int}, Amount: {amount_float}")
|
||||
return web.Response(text="YES")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Freekassa webhook: {e}")
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "enter_custom_amount_freekassa")
|
||||
async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
tg_id = callback_query.message.chat.id
|
||||
logger.info(f"User {tg_id} chose to enter a custom amount.")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_freekassa"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=ENTER_SUM,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_freekassa)
|
||||
|
||||
|
||||
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_freekassa)
|
||||
async def handle_custom_amount_input(
|
||||
message: types.Message | types.CallbackQuery,
|
||||
state: FSMContext = None,
|
||||
session: AsyncSession = None,
|
||||
):
|
||||
if isinstance(message, types.CallbackQuery):
|
||||
tg_id = message.message.chat.id
|
||||
target_message = message.message
|
||||
else:
|
||||
tg_id = message.chat.id
|
||||
target_message = message
|
||||
|
||||
logger.info(f"User {tg_id} initiated payment through Freekassa")
|
||||
|
||||
try:
|
||||
user_data = await get_temporary_data(session, tg_id)
|
||||
|
||||
if not user_data:
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Данные для оплаты не найдены. Попробуйте снова.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
state_type = user_data["state"]
|
||||
amount = user_data["data"].get("required_amount", 0)
|
||||
|
||||
if amount <= 0:
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Недостаточная сумма для пополнения.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
order_id = f"order_{tg_id}_{int(amount)}_{hash(str(tg_id) + str(amount))}"
|
||||
payment_url = generate_payment_link(amount, order_id, tg_id)
|
||||
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text="💳 Оплатить", url=payment_url))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_freekassa"))
|
||||
|
||||
if state_type == "waiting_for_payment":
|
||||
message_text = (
|
||||
f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
|
||||
)
|
||||
elif state_type == "waiting_for_renewal_payment":
|
||||
message_text = (
|
||||
f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
|
||||
)
|
||||
else:
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Некорректное состояние данных. Попробуйте снова.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
return
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text=message_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
if isinstance(state, FSMContext):
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text="Произошла ошибка при создании платежа. Попробуйте позже.",
|
||||
reply_markup=types.InlineKeyboardMarkup(),
|
||||
)
|
||||
|
||||
@@ -179,7 +179,23 @@ async def handle_pay_currency(callback_query: CallbackQuery, state: FSMContext,
|
||||
|
||||
|
||||
@router.callback_query(F.data == "balance")
|
||||
async def balance_handler(callback_query: CallbackQuery, session: AsyncSession):
|
||||
async def balance_handler(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
data = await state.get_data()
|
||||
if data.get("temp_key") and data.get("required_amount") is not None:
|
||||
from handlers.payments.fast_payment_flow import try_fast_payment_flow
|
||||
|
||||
await try_fast_payment_flow(
|
||||
callback_query,
|
||||
session,
|
||||
state,
|
||||
tg_id=callback_query.from_user.id,
|
||||
temp_key=str(data["temp_key"]),
|
||||
temp_payload=dict(data.get("temp_payload") or {}),
|
||||
required_amount=int(data["required_amount"]),
|
||||
)
|
||||
await callback_query.answer()
|
||||
return
|
||||
|
||||
stmt = select(User.balance).where(User.tg_id == callback_query.from_user.id)
|
||||
result = await session.execute(stmt)
|
||||
balance_rub = result.scalar_one_or_none() or 0.0
|
||||
|
||||
Reference in New Issue
Block a user