From 2bc7bea29dbfe1f7d90bb3ed7348efac498f77f6 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 4 Sep 2025 18:49:31 +0300 Subject: [PATCH] Enhance payment method history filtering and YooKassa integration - Updated the payment method history handler to support filtering by specific saved payment methods, improving user experience when viewing payment logs. - Increased the limit of recent payment logs retrieved from 10 to 30 for better visibility. - Enhanced the YooKassa service to include detailed payment method information, including card details and last four digits, improving clarity in payment history. - Refactored error handling in the YooKassa service to ensure robust fetching of payment information. --- bot/handlers/user/subscription.py | 41 +++++++++++++- bot/services/yookassa_service.py | 89 ++++++++++++++++--------------- 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/bot/handlers/user/subscription.py b/bot/handlers/user/subscription.py index 5cb1de5..c28288d 100644 --- a/bot/handlers/user/subscription.py +++ b/bot/handlers/user/subscription.py @@ -914,15 +914,52 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings, @router.callback_query(F.data.startswith("pm:history")) -async def payment_method_history(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession): +async def payment_method_history(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession, yookassa_service: YooKassaService): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key # Simple history from payments table filtered by user from db.dal import payment_dal - payments = await payment_dal.get_recent_payment_logs_with_user(session, limit=10, offset=0) + payments = await payment_dal.get_recent_payment_logs_with_user(session, limit=30, offset=0) user_payments = [p for p in payments if p.user_id == callback.from_user.id] + + # If viewing a specific saved payment method, filter history by that method when possible + selected_pm_provider_id: Optional[str] = None + try: + _, _, pm_id = callback.data.split(":", 2) + if pm_id: + # pm_id is our internal method_id; map to provider id + from db.dal.user_billing_dal import list_user_payment_methods + methods = await list_user_payment_methods(session, callback.from_user.id) + sel = next((m for m in methods if str(m.method_id) == pm_id), None) + if sel and sel.provider_payment_method_id: + selected_pm_provider_id = sel.provider_payment_method_id + except Exception: + selected_pm_provider_id = None + + if selected_pm_provider_id: + # Filter to rows we can confidently associate with the selected method + # Heuristics: + # 1) Payments with yookassa_payment_id -> fetch payment info and compare payment_method.id + # 2) For auto-renew description, it always uses default saved method; keep those too + filtered: List[Payment] = [] + for p in user_payments: + if p.provider != 'yookassa': + continue + if p.yookassa_payment_id and yookassa_service: + try: + info = await yookassa_service.get_payment_info(p.yookassa_payment_id) + pm = (info or {}).get("payment_method") or {} + if pm.get("id") == selected_pm_provider_id: + filtered.append(p) + continue + except Exception: + pass + # Fallback: auto-renew entries initiated via default method; include them + if (p.description or "").lower().startswith("auto-renewal"): + filtered.append(p) + user_payments = filtered if not user_payments: # Try to get pm_id from context to go one step back pm_id = "" diff --git a/bot/services/yookassa_service.py b/bot/services/yookassa_service.py index 50d61e8..d902f79 100644 --- a/bot/services/yookassa_service.py +++ b/bot/services/yookassa_service.py @@ -206,19 +206,6 @@ 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}" @@ -232,37 +219,40 @@ class YooKassaService: logging.info( f"YooKassa payment info for {payment_id_in_yookassa}: Status={payment_info_yk.status}, Paid={payment_info_yk.paid}" ) + pm = getattr(payment_info_yk, 'payment_method', None) + pm_payload: Dict[str, Any] = {} + if pm: + # Collect common fields, including id and hints for last4 + pm_id = getattr(pm, 'id', None) + pm_type = getattr(pm, 'type', None) + pm_title = getattr(pm, 'title', None) + account_number = getattr(pm, 'account_number', None) or getattr(pm, 'account', None) + card_obj = getattr(pm, 'card', None) + last4_val = None + if card_obj and hasattr(card_obj, 'last4'): + last4_val = getattr(card_obj, 'last4') + elif isinstance(account_number, str) and len(account_number) >= 4: + last4_val = account_number[-4:] + pm_payload = { + "id": pm_id, + "type": pm_type, + "title": pm_title, + "card_last4": last4_val, + } return { - "id": - payment_info_yk.id, - "status": - payment_info_yk.status, - "paid": - payment_info_yk.paid, - "amount_value": - float(payment_info_yk.amount.value), - "amount_currency": - payment_info_yk.amount.currency, - "metadata": - payment_info_yk.metadata, - "description": - payment_info_yk.description, - "refundable": - payment_info_yk.refundable, - "created_at": - payment_info_yk.created_at.isoformat() if hasattr( - payment_info_yk.created_at, 'isoformat') else str( - payment_info_yk.created_at), - "captured_at": - payment_info_yk.captured_at.isoformat() - if payment_info_yk.captured_at and hasattr( - payment_info_yk.captured_at, 'isoformat') else None, - "payment_method_type": - payment_info_yk.payment_method.type - if payment_info_yk.payment_method else None, - "test_mode": - payment_info_yk.test - if hasattr(payment_info_yk, 'test') else None + "id": payment_info_yk.id, + "status": payment_info_yk.status, + "paid": payment_info_yk.paid, + "amount_value": float(payment_info_yk.amount.value), + "amount_currency": payment_info_yk.amount.currency, + "metadata": payment_info_yk.metadata, + "description": payment_info_yk.description, + "refundable": payment_info_yk.refundable, + "created_at": payment_info_yk.created_at.isoformat() if hasattr( + payment_info_yk.created_at, 'isoformat') else str(payment_info_yk.created_at), + "captured_at": payment_info_yk.captured_at.isoformat() if getattr(payment_info_yk, 'captured_at', None) and hasattr(payment_info_yk.captured_at, 'isoformat') else None, + "payment_method": pm_payload, + "test_mode": getattr(payment_info_yk, 'test', None), } else: logging.warning( @@ -274,3 +264,16 @@ class YooKassaService: f"YooKassa get payment info for {payment_id_in_yookassa} failed: {e}", exc_info=True) 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