From 257597ccb7e93f5ae48934e21734a9b616ea5cf0 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 4 Sep 2025 16:30:45 +0300 Subject: [PATCH] Enhance payment processing and user detail updates in YooKassa integration - Added handling for 'waiting_for_capture' event in the YooKassa webhook to manage bind-only payment flows, including saving payment methods and canceling authorizations. - Introduced a new method in the YooKassaService to cancel payments, improving error handling and logging for payment cancellations. - Updated user detail synchronization to conditionally update descriptions only when they differ from the current panel state, enhancing efficiency. --- bot/handlers/admin/sync_admin.py | 5 ++++- bot/handlers/user/payment.py | 29 +++++++++++++++++++++++++++++ bot/services/yookassa_service.py | 13 +++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/bot/handlers/admin/sync_admin.py b/bot/handlers/admin/sync_admin.py index 1428fe2..39426cb 100644 --- a/bot/handlers/admin/sync_admin.py +++ b/bot/handlers/admin/sync_admin.py @@ -144,7 +144,10 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession, existing_user.first_name or "", existing_user.last_name or "", ]) - if description_text.strip(): + # Update description only when it differs from the current one on panel + current_panel_description = (panel_user_dict.get("description") or "").strip() + desired_description = description_text.strip() + if desired_description and desired_description != current_panel_description: await panel_service.update_user_details_on_panel( panel_uuid, {"description": description_text} ) diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index b4a4ba1..83534b2 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -27,6 +27,7 @@ payment_processing_lock = asyncio.Lock() YOOKASSA_EVENT_PAYMENT_SUCCEEDED = 'payment.succeeded' YOOKASSA_EVENT_PAYMENT_CANCELED = 'payment.canceled' +YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE = 'payment.waiting_for_capture' async def process_successful_payment(session: AsyncSession, bot: Bot, @@ -451,6 +452,34 @@ async def yookassa_webhook_route(request: web.Request): session, bot, payment_dict_for_processing, i18n_instance, settings) await session.commit() + elif notification_object.event == YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE: + # Bind-only flow: save method and cancel auth if metadata has bind_only + metadata = payment_dict_for_processing.get("metadata", {}) or {} + if metadata.get("bind_only") == "1": + try: + user_id_str = metadata.get("user_id") + if user_id_str and user_id_str.isdigit(): + user_id = int(user_id_str) + payment_method = payment_dict_for_processing.get("payment_method") + if isinstance(payment_method, dict) and payment_method.get("id"): + card = payment_method.get("card") or {} + await user_billing_dal.upsert_yk_payment_method( + session, + user_id=user_id, + payment_method_id=payment_method.get("id"), + card_last4=card.get("last4"), + card_network=card.get("card_type"), + ) + await session.commit() + # Attempt to cancel the authorization to avoid charge hold + try: + yk: YooKassaService = request.app.get('yookassa_service') + if yk: + await yk.cancel_payment(payment_dict_for_processing.get("id")) + except Exception: + logging.exception("Failed to cancel bind-only payment auth") + except Exception: + logging.exception("Failed to handle bind-only waiting_for_capture webhook") except Exception as e_webhook_db_processing: await session.rollback() logging.error( diff --git a/bot/services/yookassa_service.py b/bot/services/yookassa_service.py index dd68ff2..50d61e8 100644 --- a/bot/services/yookassa_service.py +++ b/bot/services/yookassa_service.py @@ -206,6 +206,19 @@ class YooKassaService: logging.error( "YooKassa is not configured. Cannot get payment info.") return None + + async def cancel_payment(self, payment_id_in_yookassa: str) -> bool: + if not self.configured: + logging.error("YooKassa is not configured. Cannot cancel payment.") + return False + try: + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, lambda: YooKassaPayment.cancel(payment_id_in_yookassa)) + logging.info(f"Cancelled YooKassa payment {payment_id_in_yookassa}") + return True + except Exception as e: + logging.error(f"Failed to cancel YooKassa payment {payment_id_in_yookassa}: {e}") + return False try: logging.info( f"Fetching payment info from YooKassa for ID: {payment_id_in_yookassa}"