Add files via upload

This commit is contained in:
Egor
2026-02-03 03:37:43 +03:00
committed by GitHub
parent 4941fe9469
commit 06224d798d
10 changed files with 97 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ from app.config import settings
from app.database.crud.user import get_user_by_telegram_id
from app.database.database import get_db
from app.localization.texts import get_texts
from app.middlewares.global_error import schedule_error_notification
from app.services.subscription_checkout_service import (
has_subscription_checkout_draft,
should_offer_checkout_resume,
@@ -30,6 +31,22 @@ from app.utils.payment_logger import payment_logger as logger
class PaymentCommonMixin:
"""Mixin с базовой логикой, которую используют остальные платёжные блоки."""
def _schedule_error_notification(self, error: Exception, context: str) -> None:
"""Безопасно планирует отправку уведомления об ошибке в админский чат.
Этот метод можно вызывать из любого mixin, т.к. он использует self.bot
из PaymentService.
"""
bot = getattr(self, 'bot', None)
if bot:
schedule_error_notification(bot, error, context)
else:
logger.warning(
'Bot instance not available for error notification: %s - %s',
context,
error,
)
async def build_topup_success_keyboard(self, user: Any) -> InlineKeyboardMarkup:
"""Формирует клавиатуру по завершении платежа, подстраиваясь под пользователя."""
# Загружаем нужные тексты с учётом выбранного языка пользователя.

View File

@@ -91,6 +91,8 @@ class CryptoBotPaymentMixin:
if not invoice_data:
logger.error('Ошибка создания CryptoBot invoice')
error = ValueError('CryptoBot invoice creation returned empty result')
self._schedule_error_notification(error, f'CryptoBot invoice creation error: user_id={user_id}, amount={amount_usd}')
return None
cryptobot_crud = import_module('app.database.crud.cryptobot')
@@ -131,6 +133,7 @@ class CryptoBotPaymentMixin:
except Exception as error:
logger.error('Ошибка создания CryptoBot платежа: %s', error)
self._schedule_error_notification(error, f'CryptoBot payment creation exception: user_id={user_id}')
return None
async def process_cryptobot_webhook(
@@ -152,12 +155,16 @@ class CryptoBotPaymentMixin:
if not invoice_id:
logger.error('CryptoBot webhook без invoice_id')
error = ValueError('CryptoBot webhook missing invoice_id')
self._schedule_error_notification(error, 'CryptoBot webhook error: missing invoice_id')
return False
cryptobot_crud = import_module('app.database.crud.cryptobot')
payment = await cryptobot_crud.get_cryptobot_payment_by_invoice_id(db, invoice_id)
if not payment:
logger.error('CryptoBot платеж не найден в БД: %s', invoice_id)
error = ValueError(f'CryptoBot payment not found: {invoice_id}')
self._schedule_error_notification(error, f'CryptoBot webhook error: payment not found for invoice_id={invoice_id}')
return False
if payment.status == 'paid':
@@ -237,6 +244,8 @@ class CryptoBotPaymentMixin:
amount_kopeks,
invoice_id,
)
error = ValueError(f'Invalid amount after conversion: {amount_kopeks} kopeks')
self._schedule_error_notification(error, f'CryptoBot webhook error: invalid amount for invoice_id={invoice_id}')
return False
payment_service_module = import_module('app.services.payment_service')
@@ -263,6 +272,8 @@ class CryptoBotPaymentMixin:
'Пользователь с ID %s не найден при пополнении баланса',
updated_payment.user_id,
)
error = ValueError(f'User not found: {updated_payment.user_id}')
self._schedule_error_notification(error, f'CryptoBot webhook error: user not found for invoice_id={invoice_id}')
return False
old_balance = user.balance_kopeks
@@ -433,6 +444,7 @@ class CryptoBotPaymentMixin:
except Exception as error:
logger.error('Ошибка обработки CryptoBot webhook: %s', error, exc_info=True)
self._schedule_error_notification(error, 'CryptoBot webhook processing exception')
return False
async def _process_subscription_renewal_payment(

View File

@@ -268,6 +268,8 @@ class FreekassaPaymentMixin:
payment.order_id,
trigger,
)
error = ValueError(f'User not found: {payment.user_id}')
self._schedule_error_notification(error, f'Freekassa finalize error: user not found for order_id={payment.order_id}')
return False
# Создаем транзакцию

View File

@@ -95,11 +95,15 @@ class HeleketPaymentMixin:
if not response:
logger.error('Heleket API вернул пустой ответ при создании платежа')
error = ValueError('Heleket API returned empty response')
self._schedule_error_notification(error, f'Heleket payment creation error: user_id={user_id}')
return None
payment_result = response.get('result') if isinstance(response, dict) else None
if not payment_result:
logger.error('Некорректный ответ Heleket API: %s', response)
error = ValueError(f'Invalid Heleket API response: {response}')
self._schedule_error_notification(error, f'Heleket payment creation error: user_id={user_id}')
return None
uuid = str(payment_result.get('uuid'))
@@ -181,6 +185,8 @@ class HeleketPaymentMixin:
) -> HeleketPayment | None:
if not isinstance(payload, dict):
logger.error('Heleket webhook payload не является словарём: %s', payload)
error = ValueError(f'Heleket webhook payload is not a dict: {type(payload)}')
self._schedule_error_notification(error, 'Heleket webhook error: invalid payload type')
return None
heleket_crud = import_module('app.database.crud.heleket')
@@ -192,6 +198,8 @@ class HeleketPaymentMixin:
if not uuid and not order_id:
logger.error('Heleket webhook без uuid/order_id: %s', payload)
error = ValueError('Heleket webhook missing uuid and order_id')
self._schedule_error_notification(error, 'Heleket webhook error: missing identifiers')
return None
payment = None
@@ -206,6 +214,8 @@ class HeleketPaymentMixin:
uuid,
order_id,
)
error = ValueError(f'Heleket payment not found: uuid={uuid}, order_id={order_id}')
self._schedule_error_notification(error, f'Heleket webhook error: payment not found uuid={uuid}')
return None
payer_amount = payload.get('payer_amount') or payload.get('payment_amount')
@@ -309,6 +319,8 @@ class HeleketPaymentMixin:
amount_kopeks = updated_payment.amount_kopeks
if amount_kopeks <= 0:
logger.error('Heleket платеж %s имеет некорректную сумму: %s', updated_payment.uuid, updated_payment.amount)
error = ValueError(f'Heleket payment has invalid amount: {updated_payment.amount}')
self._schedule_error_notification(error, f'Heleket webhook error: invalid amount for uuid={updated_payment.uuid}')
return None
transaction = await payment_module.create_transaction(
@@ -338,6 +350,8 @@ class HeleketPaymentMixin:
user = await get_user_by_id(db, updated_payment.user_id)
if not user:
logger.error('Пользователь %s не найден для Heleket платежа', updated_payment.user_id)
error = ValueError(f'User not found: {updated_payment.user_id}')
self._schedule_error_notification(error, f'Heleket webhook error: user not found for uuid={updated_payment.uuid}')
return None
old_balance = user.balance_kopeks

View File

@@ -104,6 +104,8 @@ class KassaAiPaymentMixin:
payment_url = result.get('location')
if not payment_url:
logger.error('KassaAI API не вернул URL платежа')
error = ValueError(f'KassaAI API missing payment URL: {result}')
self._schedule_error_notification(error, f'KassaAI payment creation error: user_id={user_id}')
return None
logger.info(
@@ -261,6 +263,8 @@ class KassaAiPaymentMixin:
payment.order_id,
trigger,
)
error = ValueError(f'User not found: {payment.user_id}')
self._schedule_error_notification(error, f'KassaAI finalize error: user not found for order_id={payment.order_id}')
return False
# Создаем транзакцию

View File

@@ -81,6 +81,8 @@ class MulenPayPaymentMixin:
if not response:
logger.error('Ошибка создания %s платежа', display_name)
error = ValueError(f'{display_name} payment creation returned empty response')
self._schedule_error_notification(error, f'{display_name} payment creation error: user_id={user_id}')
return None
mulen_payment_id = response.get('id')
@@ -124,6 +126,7 @@ class MulenPayPaymentMixin:
except Exception as error:
logger.error('Ошибка создания %s платежа: %s', display_name, error)
self._schedule_error_notification(error, f'{display_name} payment creation exception: user_id={user_id}')
return None
async def process_mulenpay_callback(
@@ -159,6 +162,8 @@ class MulenPayPaymentMixin:
if not uuid_value and mulen_payment_id_raw is None:
logger.error('%s callback без uuid и id', display_name)
error = ValueError(f'{display_name} callback missing uuid and id')
self._schedule_error_notification(error, f'{display_name} webhook error: missing identifiers')
return False
payment = None
@@ -175,6 +180,8 @@ class MulenPayPaymentMixin:
uuid_value,
mulen_payment_id_raw,
)
error = ValueError(f'{display_name} payment not found: uuid={uuid_value}, id={mulen_payment_id_raw}')
self._schedule_error_notification(error, f'{display_name} webhook error: payment not found')
return False
metadata = dict(getattr(payment, 'metadata_json', {}) or {})
@@ -269,6 +276,8 @@ class MulenPayPaymentMixin:
payment.user_id,
display_name,
)
error = ValueError(f'User not found: {payment.user_id}')
self._schedule_error_notification(error, f'{display_name} webhook error: user not found for uuid={payment.uuid}')
return False
old_balance = user.balance_kopeks
@@ -495,6 +504,7 @@ class MulenPayPaymentMixin:
error,
exc_info=True,
)
self._schedule_error_notification(error, f'{display_name} webhook processing exception')
return False
def _map_mulenpay_status(self, status_code: int | None) -> str:

View File

@@ -83,15 +83,20 @@ class Pal24PaymentMixin:
)
except Pal24APIError as error:
logger.error('Ошибка Pal24 API при создании счета: %s', error)
self._schedule_error_notification(error, f'Pal24 payment creation API error: user_id={user_id}')
return None
if not response.get('success', True):
logger.error('Pal24 вернул ошибку при создании счета: %s', response)
error = ValueError(f'Pal24 payment creation failed: {response}')
self._schedule_error_notification(error, f'Pal24 payment creation error: user_id={user_id}')
return None
bill_id = response.get('bill_id')
if not bill_id:
logger.error('Pal24 не вернул bill_id: %s', response)
error = ValueError(f'Pal24 missing bill_id: {response}')
self._schedule_error_notification(error, f'Pal24 payment creation error: user_id={user_id}')
return None
def _pick_url(*keys: str) -> str | None:
@@ -232,6 +237,8 @@ class Pal24PaymentMixin:
if not bill_id and not order_id:
logger.error('Pal24 callback без идентификаторов: %s', callback)
error = ValueError('Pal24 callback missing identifiers')
self._schedule_error_notification(error, 'Pal24 webhook error: missing identifiers')
return False
payment = None
@@ -242,6 +249,8 @@ class Pal24PaymentMixin:
if not payment:
logger.error('Pal24 платеж не найден: %s / %s', bill_id, order_id)
error = ValueError(f'Pal24 payment not found: bill_id={bill_id}, order_id={order_id}')
self._schedule_error_notification(error, f'Pal24 webhook error: payment not found bill_id={bill_id}')
return False
if payment.is_paid:
@@ -310,6 +319,7 @@ class Pal24PaymentMixin:
except Exception as error:
logger.error('Ошибка обработки Pal24 callback: %s', error, exc_info=True)
self._schedule_error_notification(error, 'Pal24 webhook processing exception')
return False
async def _finalize_pal24_payment(
@@ -375,6 +385,8 @@ class Pal24PaymentMixin:
payment.bill_id,
trigger,
)
error = ValueError(f'User not found: {payment.user_id}')
self._schedule_error_notification(error, f'Pal24 finalize error: user not found for bill_id={payment.bill_id}')
return False
transaction = await payment_module.create_transaction(

View File

@@ -75,10 +75,13 @@ class PlategaPaymentMixin:
)
except Exception as error: # pragma: no cover - network errors
logger.exception('Ошибка Platega при создании платежа: %s', error)
self._schedule_error_notification(error, f'Platega payment creation exception: user_id={user_id}')
return None
if not response:
logger.error('Platega вернул пустой ответ при создании платежа')
error = ValueError('Platega payment creation returned empty response')
self._schedule_error_notification(error, f'Platega payment creation error: user_id={user_id}')
return None
transaction_id = response.get('transactionId') or response.get('id')
@@ -320,6 +323,8 @@ class PlategaPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error('Пользователь %s не найден для Platega', payment.user_id)
error = ValueError(f'User not found: {payment.user_id}')
self._schedule_error_notification(error, f'Platega finalize error: user not found for correlation_id={payment.correlation_id}')
return payment
# Убеждаемся, что промогруппы загружены в асинхронном контексте,

View File

@@ -111,9 +111,11 @@ class WataPaymentMixin:
)
except WataAPIError as error:
logger.error('Ошибка создания WATA платежа: %s', error)
self._schedule_error_notification(error, f'WATA payment creation API error: user_id={user_id}')
return None
except Exception as error: # pragma: no cover - safety net
logger.exception('Непредвиденная ошибка при создании WATA платежа: %s', error)
self._schedule_error_notification(error, f'WATA payment creation exception: user_id={user_id}')
return None
payment_link_id = response.get('id')
@@ -125,6 +127,8 @@ class WataPaymentMixin:
if not payment_link_id:
logger.error('WATA API не вернула идентификатор платежной ссылки: %s', response)
error = ValueError(f'WATA API missing payment_link_id: {response}')
self._schedule_error_notification(error, f'WATA payment creation error: user_id={user_id}')
return None
expiration_raw = response.get('expirationDateTime')
@@ -179,6 +183,8 @@ class WataPaymentMixin:
if not isinstance(payload, dict):
logger.error('WATA webhook payload не является словарём: %s', payload)
error = ValueError(f'WATA webhook payload is not a dict: {type(payload)}')
self._schedule_error_notification(error, 'WATA webhook error: invalid payload type')
return False
order_id_raw = payload.get('orderId')
@@ -194,10 +200,14 @@ class WataPaymentMixin:
'WATA webhook без orderId и paymentLinkId: %s',
payload,
)
error = ValueError('WATA webhook missing orderId and paymentLinkId')
self._schedule_error_notification(error, 'WATA webhook error: missing identifiers')
return False
if not transaction_status:
logger.error('WATA webhook без статуса транзакции: %s', payload)
error = ValueError('WATA webhook missing transactionStatus')
self._schedule_error_notification(error, 'WATA webhook error: missing status')
return False
payment = None
@@ -212,6 +222,8 @@ class WataPaymentMixin:
order_id,
payment_link_id,
)
error = ValueError(f'WATA payment not found: order_id={order_id}, payment_link_id={payment_link_id}')
self._schedule_error_notification(error, f'WATA webhook error: payment not found order_id={order_id}')
return False
status_lower = transaction_status.lower()
@@ -448,6 +460,8 @@ class WataPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error('Пользователь %s не найден при обработке WATA', payment.user_id)
error = ValueError(f'User not found: {payment.user_id}')
self._schedule_error_notification(error, f'WATA finalize error: user not found for payment_link_id={payment.payment_link_id}')
return payment
transaction_external_id = str(transaction_payload.get('id') or transaction_payload.get('transactionId') or '')

View File

@@ -145,6 +145,8 @@ class YooKassaPaymentMixin:
if not yookassa_response or yookassa_response.get('error'):
logger.error('Ошибка создания платежа YooKassa: %s', yookassa_response)
error = ValueError(f'YooKassa payment creation failed: {yookassa_response}')
self._schedule_error_notification(error, f'YooKassa payment creation error: user_id={user_id}, amount={amount_kopeks}')
return None
yookassa_created_at: datetime | None = None
@@ -190,6 +192,7 @@ class YooKassaPaymentMixin:
except Exception as error:
logger.error('Ошибка создания платежа YooKassa: %s', error)
self._schedule_error_notification(error, f'YooKassa payment creation exception: user_id={user_id}')
return None
async def create_yookassa_sbp_payment(
@@ -250,6 +253,8 @@ class YooKassaPaymentMixin:
'Ошибка создания платежа YooKassa СБП: %s',
yookassa_response,
)
error = ValueError(f'YooKassa SBP payment creation failed: {yookassa_response}')
self._schedule_error_notification(error, f'YooKassa SBP payment creation error: user_id={user_id}')
return None
local_payment = await payment_module.create_yookassa_payment(
@@ -290,6 +295,7 @@ class YooKassaPaymentMixin:
except Exception as error:
logger.error('Ошибка создания платежа YooKassa СБП: %s', error)
self._schedule_error_notification(error, f'YooKassa SBP payment creation exception: user_id={user_id}')
return None
async def get_yookassa_payment_status(
@@ -1108,6 +1114,7 @@ class YooKassaPaymentMixin:
payment.yookassa_payment_id,
error,
)
self._schedule_error_notification(error, f'YooKassa successful payment processing error: payment_id={payment.yookassa_payment_id}')
return False
async def _mark_yookassa_payment_processing_completed(