diff --git a/app/services/payment/common.py b/app/services/payment/common.py index a4a2d3d7..ddd3aefa 100644 --- a/app/services/payment/common.py +++ b/app/services/payment/common.py @@ -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: """Формирует клавиатуру по завершении платежа, подстраиваясь под пользователя.""" # Загружаем нужные тексты с учётом выбранного языка пользователя. diff --git a/app/services/payment/cryptobot.py b/app/services/payment/cryptobot.py index 93975cf7..9633cfbf 100644 --- a/app/services/payment/cryptobot.py +++ b/app/services/payment/cryptobot.py @@ -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( diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py index 4533cce7..6a25aa66 100644 --- a/app/services/payment/freekassa.py +++ b/app/services/payment/freekassa.py @@ -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 # Создаем транзакцию diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py index 310e75f2..cc0a9865 100644 --- a/app/services/payment/heleket.py +++ b/app/services/payment/heleket.py @@ -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 diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py index 1cc3818d..9c0bf707 100644 --- a/app/services/payment/kassa_ai.py +++ b/app/services/payment/kassa_ai.py @@ -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 # Создаем транзакцию diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py index f41befcf..68fc225f 100644 --- a/app/services/payment/mulenpay.py +++ b/app/services/payment/mulenpay.py @@ -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: diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 9834bb8a..e9cb6626 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -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( diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py index e83bd31b..36be7c0c 100644 --- a/app/services/payment/platega.py +++ b/app/services/payment/platega.py @@ -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 # Убеждаемся, что промогруппы загружены в асинхронном контексте, diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py index 98c2f760..14446089 100644 --- a/app/services/payment/wata.py +++ b/app/services/payment/wata.py @@ -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 '') diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 5358f283..08e46fc2 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -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(