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.
This commit is contained in:
machka-pasla
2025-09-04 18:49:31 +03:00
parent b6a2bf2329
commit 2bc7bea29d
2 changed files with 85 additions and 45 deletions
+39 -2
View File
@@ -914,15 +914,52 @@ async def payment_method_view(callback: types.CallbackQuery, settings: Settings,
@router.callback_query(F.data.startswith("pm:history")) @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) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
# Simple history from payments table filtered by user # Simple history from payments table filtered by user
from db.dal import payment_dal 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] 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: if not user_payments:
# Try to get pm_id from context to go one step back # Try to get pm_id from context to go one step back
pm_id = "" pm_id = ""
+46 -43
View File
@@ -206,19 +206,6 @@ class YooKassaService:
logging.error( logging.error(
"YooKassa is not configured. Cannot get payment info.") "YooKassa is not configured. Cannot get payment info.")
return None 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: try:
logging.info( logging.info(
f"Fetching payment info from YooKassa for ID: {payment_id_in_yookassa}" f"Fetching payment info from YooKassa for ID: {payment_id_in_yookassa}"
@@ -232,37 +219,40 @@ class YooKassaService:
logging.info( logging.info(
f"YooKassa payment info for {payment_id_in_yookassa}: Status={payment_info_yk.status}, Paid={payment_info_yk.paid}" 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 { return {
"id": "id": payment_info_yk.id,
payment_info_yk.id, "status": payment_info_yk.status,
"status": "paid": payment_info_yk.paid,
payment_info_yk.status, "amount_value": float(payment_info_yk.amount.value),
"paid": "amount_currency": payment_info_yk.amount.currency,
payment_info_yk.paid, "metadata": payment_info_yk.metadata,
"amount_value": "description": payment_info_yk.description,
float(payment_info_yk.amount.value), "refundable": payment_info_yk.refundable,
"amount_currency": "created_at": payment_info_yk.created_at.isoformat() if hasattr(
payment_info_yk.amount.currency, payment_info_yk.created_at, 'isoformat') else str(payment_info_yk.created_at),
"metadata": "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_info_yk.metadata, "payment_method": pm_payload,
"description": "test_mode": getattr(payment_info_yk, 'test', None),
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
} }
else: else:
logging.warning( logging.warning(
@@ -274,3 +264,16 @@ class YooKassaService:
f"YooKassa get payment info for {payment_id_in_yookassa} failed: {e}", f"YooKassa get payment info for {payment_id_in_yookassa} failed: {e}",
exc_info=True) exc_info=True)
return None 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