cd8be32671
The admin Payments page filter and pending payments tab were missing newer providers (paypear, rollypay, aurapay, etoplatezhi, antilopay, jupiter, donut, lava) because they were never registered in the search/verification registries. Customer payments via these providers were invisible in admin filtering. payment_search_service.py: - Add 8 _search_<provider> functions matching the existing pattern - Register them in _PROVIDER_SEARCH_MAP (now 22 methods total) - Filter dropdown and stats.by_method now include all providers payment_verification_service.py: - Add 8 _is_<provider>_pending and _fetch_<provider>_payments functions - Wire them into list_recent_pending_payments and get_payment_record - Add display name and is_enabled dispatch branches for all 8 - Register paypear / rollypay / aurapay in SUPPORTED_MANUAL_CHECK_METHODS and SUPPORTED_AUTO_CHECK_METHODS (they have full API+DB sync via check_*) - Wire them into run_manual_check - etoplatezhi / antilopay / jupiter / donut / lava remain webhook-driven and appear in pending tab without manual-check button (no fake API sync)
1587 lines
56 KiB
Python
1587 lines
56 KiB
Python
"""Helpers for inspecting and manually checking pending top-up payments."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import re
|
||
from collections import Counter
|
||
from collections.abc import Iterable
|
||
from dataclasses import dataclass
|
||
from datetime import UTC, datetime, timedelta
|
||
from typing import TYPE_CHECKING, Any
|
||
|
||
import structlog
|
||
from sqlalchemy import desc, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.config import settings
|
||
from app.database.database import AsyncSessionLocal
|
||
from app.database.models import (
|
||
AntilopayPayment,
|
||
AuraPayPayment,
|
||
CloudPaymentsPayment,
|
||
CryptoBotPayment,
|
||
DonutPayment,
|
||
EtoplatezhiPayment,
|
||
FreekassaPayment,
|
||
HeleketPayment,
|
||
JupiterPayment,
|
||
KassaAiPayment,
|
||
LavaPayment,
|
||
MulenPayPayment,
|
||
Pal24Payment,
|
||
PaymentMethod,
|
||
PayPearPayment,
|
||
PlategaPayment,
|
||
RioPayPayment,
|
||
RollyPayPayment,
|
||
SeverPayPayment,
|
||
Transaction,
|
||
TransactionType,
|
||
User,
|
||
WataPayment,
|
||
YooKassaPayment,
|
||
)
|
||
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
PENDING_MAX_AGE = timedelta(hours=24)
|
||
|
||
|
||
@dataclass(slots=True)
|
||
class PendingPayment:
|
||
"""Normalized representation of a provider specific payment entry."""
|
||
|
||
method: PaymentMethod
|
||
local_id: int
|
||
identifier: str
|
||
amount_kopeks: int
|
||
status: str
|
||
is_paid: bool
|
||
created_at: datetime
|
||
user: User
|
||
payment: Any
|
||
expires_at: datetime | None = None
|
||
|
||
def is_recent(self, max_age: timedelta = PENDING_MAX_AGE) -> bool:
|
||
return (datetime.now(UTC) - self.created_at) <= max_age
|
||
|
||
|
||
SUPPORTED_MANUAL_CHECK_METHODS: frozenset[PaymentMethod] = frozenset(
|
||
{
|
||
PaymentMethod.YOOKASSA,
|
||
PaymentMethod.MULENPAY,
|
||
PaymentMethod.PAL24,
|
||
PaymentMethod.WATA,
|
||
PaymentMethod.HELEKET,
|
||
PaymentMethod.CRYPTOBOT,
|
||
PaymentMethod.PLATEGA,
|
||
PaymentMethod.CLOUDPAYMENTS,
|
||
PaymentMethod.FREEKASSA,
|
||
PaymentMethod.KASSA_AI,
|
||
PaymentMethod.RIOPAY,
|
||
PaymentMethod.SEVERPAY,
|
||
PaymentMethod.OVERPAY,
|
||
PaymentMethod.PAYPEAR,
|
||
PaymentMethod.ROLLYPAY,
|
||
PaymentMethod.AURAPAY,
|
||
# ETOPLATEZHI / ANTILOPAY / JUPITER / DONUT / LAVA — webhook-driven,
|
||
# без API-метода синхронизации БД, manual check не реализован.
|
||
}
|
||
)
|
||
|
||
|
||
SUPPORTED_AUTO_CHECK_METHODS: frozenset[PaymentMethod] = frozenset(
|
||
{
|
||
PaymentMethod.YOOKASSA,
|
||
PaymentMethod.MULENPAY,
|
||
PaymentMethod.PAL24,
|
||
PaymentMethod.CRYPTOBOT,
|
||
PaymentMethod.PLATEGA,
|
||
PaymentMethod.HELEKET,
|
||
# CloudPayments removed - API returns "Completed" during authorization
|
||
# before final result, causing premature balance credits. Webhooks work correctly.
|
||
# WATA removed - API returns 429 "Use webhook – polling is rate-limited".
|
||
# Payments are processed via webhook (wata_webhook.py).
|
||
PaymentMethod.FREEKASSA,
|
||
PaymentMethod.KASSA_AI,
|
||
PaymentMethod.RIOPAY,
|
||
PaymentMethod.SEVERPAY,
|
||
PaymentMethod.OVERPAY,
|
||
PaymentMethod.PAYPEAR,
|
||
PaymentMethod.ROLLYPAY,
|
||
PaymentMethod.AURAPAY,
|
||
}
|
||
)
|
||
|
||
|
||
def method_display_name(method: PaymentMethod) -> str:
|
||
if method == PaymentMethod.MULENPAY:
|
||
return settings.get_mulenpay_display_name()
|
||
if method == PaymentMethod.PAL24:
|
||
return 'PayPalych'
|
||
if method == PaymentMethod.YOOKASSA:
|
||
return 'YooKassa'
|
||
if method == PaymentMethod.WATA:
|
||
return 'WATA'
|
||
if method == PaymentMethod.PLATEGA:
|
||
return settings.get_platega_display_name()
|
||
if method == PaymentMethod.CRYPTOBOT:
|
||
return 'CryptoBot'
|
||
if method == PaymentMethod.HELEKET:
|
||
return 'Heleket'
|
||
if method == PaymentMethod.CLOUDPAYMENTS:
|
||
return 'CloudPayments'
|
||
if method == PaymentMethod.FREEKASSA:
|
||
return 'Freekassa'
|
||
if method == PaymentMethod.KASSA_AI:
|
||
return settings.get_kassa_ai_display_name()
|
||
if method == PaymentMethod.RIOPAY:
|
||
return settings.get_riopay_display_name()
|
||
if method == PaymentMethod.SEVERPAY:
|
||
return settings.get_severpay_display_name()
|
||
if method == PaymentMethod.OVERPAY:
|
||
return settings.get_overpay_display_name()
|
||
if method == PaymentMethod.PAYPEAR:
|
||
return settings.get_paypear_display_name()
|
||
if method == PaymentMethod.ROLLYPAY:
|
||
return settings.get_rollypay_display_name()
|
||
if method == PaymentMethod.AURAPAY:
|
||
return settings.get_aurapay_display_name()
|
||
if method == PaymentMethod.ETOPLATEZHI:
|
||
return settings.get_etoplatezhi_display_name()
|
||
if method == PaymentMethod.ANTILOPAY:
|
||
return settings.get_antilopay_display_name()
|
||
if method == PaymentMethod.JUPITER:
|
||
return settings.get_jupiter_display_name()
|
||
if method == PaymentMethod.DONUT:
|
||
return settings.get_donut_display_name()
|
||
if method == PaymentMethod.LAVA:
|
||
return settings.get_lava_display_name()
|
||
if method == PaymentMethod.TELEGRAM_STARS:
|
||
return 'Telegram Stars'
|
||
return method.value
|
||
|
||
|
||
def _method_is_enabled(method: PaymentMethod) -> bool:
|
||
if method == PaymentMethod.YOOKASSA:
|
||
return settings.is_yookassa_enabled()
|
||
if method == PaymentMethod.MULENPAY:
|
||
return settings.is_mulenpay_enabled()
|
||
if method == PaymentMethod.PAL24:
|
||
return settings.is_pal24_enabled()
|
||
if method == PaymentMethod.WATA:
|
||
return settings.is_wata_enabled()
|
||
if method == PaymentMethod.PLATEGA:
|
||
return settings.is_platega_enabled()
|
||
if method == PaymentMethod.CRYPTOBOT:
|
||
return settings.is_cryptobot_enabled()
|
||
if method == PaymentMethod.HELEKET:
|
||
return settings.is_heleket_enabled()
|
||
if method == PaymentMethod.CLOUDPAYMENTS:
|
||
return settings.is_cloudpayments_enabled()
|
||
if method == PaymentMethod.FREEKASSA:
|
||
return settings.is_freekassa_enabled()
|
||
if method == PaymentMethod.KASSA_AI:
|
||
return settings.is_kassa_ai_enabled()
|
||
if method == PaymentMethod.RIOPAY:
|
||
return settings.is_riopay_enabled()
|
||
if method == PaymentMethod.SEVERPAY:
|
||
return settings.is_severpay_enabled()
|
||
if method == PaymentMethod.OVERPAY:
|
||
return settings.is_overpay_enabled()
|
||
if method == PaymentMethod.PAYPEAR:
|
||
return settings.is_paypear_enabled()
|
||
if method == PaymentMethod.ROLLYPAY:
|
||
return settings.is_rollypay_enabled()
|
||
if method == PaymentMethod.AURAPAY:
|
||
return settings.is_aurapay_enabled()
|
||
if method == PaymentMethod.ETOPLATEZHI:
|
||
return settings.is_etoplatezhi_enabled()
|
||
if method == PaymentMethod.ANTILOPAY:
|
||
return settings.is_antilopay_enabled()
|
||
if method == PaymentMethod.JUPITER:
|
||
return settings.is_jupiter_enabled()
|
||
if method == PaymentMethod.DONUT:
|
||
return settings.is_donut_enabled()
|
||
if method == PaymentMethod.LAVA:
|
||
return settings.is_lava_enabled()
|
||
return False
|
||
|
||
|
||
def get_enabled_auto_methods() -> list[PaymentMethod]:
|
||
return [method for method in SUPPORTED_AUTO_CHECK_METHODS if _method_is_enabled(method)]
|
||
|
||
|
||
class AutoPaymentVerificationService:
|
||
"""Background checker that periodically refreshes pending payments."""
|
||
|
||
def __init__(self) -> None:
|
||
self._task: asyncio.Task[None] | None = None
|
||
self._payment_service: PaymentService | None = None
|
||
|
||
def set_payment_service(self, payment_service: PaymentService) -> None:
|
||
self._payment_service = payment_service
|
||
|
||
def is_running(self) -> bool:
|
||
return self._task is not None and not self._task.done()
|
||
|
||
async def start(self) -> None:
|
||
await self.stop()
|
||
|
||
if not settings.is_payment_verification_auto_check_enabled():
|
||
logger.info('Автопроверка пополнений отключена настройками')
|
||
return
|
||
|
||
if not self._payment_service:
|
||
logger.warning('Автопроверка пополнений не запущена: PaymentService не инициализирован')
|
||
return
|
||
|
||
methods = get_enabled_auto_methods()
|
||
if not methods:
|
||
logger.info('Автопроверка пополнений не запущена: нет активных провайдеров')
|
||
return
|
||
|
||
display_names = ', '.join(sorted(method_display_name(method) for method in methods))
|
||
interval_minutes = settings.get_payment_verification_auto_check_interval()
|
||
|
||
self._task = asyncio.create_task(self._auto_check_loop())
|
||
logger.info(
|
||
'🔄 Автопроверка пополнений запущена (каждые мин) для',
|
||
interval_minutes=interval_minutes,
|
||
display_names=display_names,
|
||
)
|
||
|
||
async def stop(self) -> None:
|
||
if self._task and not self._task.done():
|
||
self._task.cancel()
|
||
try:
|
||
await self._task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
self._task = None
|
||
|
||
async def _auto_check_loop(self) -> None:
|
||
try:
|
||
while True:
|
||
interval_minutes = settings.get_payment_verification_auto_check_interval()
|
||
try:
|
||
if settings.is_payment_verification_auto_check_enabled() and self._payment_service:
|
||
methods = get_enabled_auto_methods()
|
||
if methods:
|
||
await self._run_checks(methods)
|
||
else:
|
||
logger.debug('Автопроверка пополнений: активных провайдеров нет')
|
||
else:
|
||
logger.debug('Автопроверка пополнений: отключена настройками или сервис не готов')
|
||
except asyncio.CancelledError:
|
||
raise
|
||
except Exception as error:
|
||
logger.error('Ошибка автопроверки пополнений', error=error, exc_info=True)
|
||
|
||
await asyncio.sleep(max(1, interval_minutes) * 60)
|
||
except asyncio.CancelledError:
|
||
logger.info('Автопроверка пополнений остановлена')
|
||
raise
|
||
|
||
async def _run_checks(self, methods: list[PaymentMethod]) -> None:
|
||
if not self._payment_service:
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
try:
|
||
pending = await list_recent_pending_payments(session)
|
||
candidates = [record for record in pending if record.method in methods and not record.is_paid]
|
||
|
||
if not candidates:
|
||
logger.debug('Автопроверка пополнений: подходящих ожидающих платежей нет')
|
||
return
|
||
|
||
counts = Counter(record.method for record in candidates)
|
||
summary = ', '.join(
|
||
f'{method_display_name(method)}: {count}'
|
||
for method, count in sorted(counts.items(), key=lambda item: method_display_name(item[0]))
|
||
)
|
||
logger.info(
|
||
'🔄 Автопроверка пополнений: найдено инвойсов', candidates_count=len(candidates), summary=summary
|
||
)
|
||
|
||
for record in candidates:
|
||
try:
|
||
refreshed = await run_manual_check(
|
||
session,
|
||
record.method,
|
||
record.local_id,
|
||
self._payment_service,
|
||
)
|
||
except Exception as check_error:
|
||
logger.error(
|
||
'Ошибка проверки платежа, откатываем сессию',
|
||
method_display_name=method_display_name(record.method),
|
||
identifier=record.identifier,
|
||
error=check_error,
|
||
)
|
||
if session.in_transaction():
|
||
await session.rollback()
|
||
continue
|
||
|
||
if not refreshed:
|
||
logger.debug(
|
||
'Автопроверка пополнений: не удалось обновить',
|
||
method_display_name=method_display_name(record.method),
|
||
identifier=record.identifier,
|
||
)
|
||
continue
|
||
|
||
if refreshed.is_paid and not record.is_paid:
|
||
logger.info(
|
||
'✅ отмечен как оплаченный после автопроверки',
|
||
method_display_name=method_display_name(refreshed.method),
|
||
identifier=refreshed.identifier,
|
||
)
|
||
elif refreshed.status != record.status:
|
||
logger.info(
|
||
'ℹ️ обновлён: →',
|
||
method_display_name=method_display_name(refreshed.method),
|
||
identifier=refreshed.identifier,
|
||
record_status=record.status or '—',
|
||
refreshed_status=refreshed.status or '—',
|
||
)
|
||
else:
|
||
logger.debug(
|
||
'Автопроверка пополнений: без изменений',
|
||
method_display_name=method_display_name(refreshed.method),
|
||
identifier=refreshed.identifier,
|
||
refreshed_status=refreshed.status or '—',
|
||
)
|
||
|
||
if session.in_transaction():
|
||
await session.commit()
|
||
except Exception:
|
||
if session.in_transaction():
|
||
await session.rollback()
|
||
raise
|
||
|
||
|
||
auto_payment_verification_service = AutoPaymentVerificationService()
|
||
|
||
|
||
def _is_pal24_pending(payment: Pal24Payment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').upper()
|
||
return status in {'NEW', 'PROCESS'}
|
||
|
||
|
||
def _is_mulenpay_pending(payment: MulenPayPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'created', 'processing', 'hold'}
|
||
|
||
|
||
def _is_wata_pending(payment: WataPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status not in {
|
||
'paid',
|
||
'closed',
|
||
'declined',
|
||
'canceled',
|
||
'cancelled',
|
||
'expired',
|
||
}
|
||
|
||
|
||
def _is_platega_pending(payment: PlategaPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'inprogress', 'in_progress'}
|
||
|
||
|
||
def _is_heleket_pending(payment: HeleketPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status not in {'paid', 'paid_over', 'cancel', 'canceled', 'failed', 'fail', 'expired'}
|
||
|
||
|
||
def _is_yookassa_pending(payment: YooKassaPayment) -> bool:
|
||
if getattr(payment, 'is_paid', False) and payment.status == 'succeeded':
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'waiting_for_capture'}
|
||
|
||
|
||
def _is_cryptobot_pending(payment: CryptoBotPayment) -> bool:
|
||
status = (payment.status or '').lower()
|
||
return status == 'active'
|
||
|
||
|
||
def _is_cloudpayments_pending(payment: CloudPaymentsPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'authorized'}
|
||
|
||
|
||
def _is_freekassa_pending(payment: FreekassaPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_kassa_ai_pending(payment: KassaAiPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_severpay_pending(payment: SeverPayPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'processing'}
|
||
|
||
|
||
def _is_riopay_pending(payment: RioPayPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending'}
|
||
|
||
|
||
def _is_paypear_pending(payment: PayPearPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_rollypay_pending(payment: RollyPayPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_aurapay_pending(payment: AuraPayPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_etoplatezhi_pending(payment: EtoplatezhiPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_antilopay_pending(payment: AntilopayPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_jupiter_pending(payment: JupiterPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_donut_pending(payment: DonutPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _is_lava_pending(payment: LavaPayment) -> bool:
|
||
if payment.is_paid:
|
||
return False
|
||
status = (payment.status or '').lower()
|
||
return status in {'pending', 'created', 'processing'}
|
||
|
||
|
||
def _parse_cryptobot_amount_kopeks(payment: CryptoBotPayment) -> int:
|
||
payload = payment.payload or ''
|
||
match = re.search(r'_(\d+)$', payload)
|
||
if match:
|
||
try:
|
||
return int(match.group(1))
|
||
except ValueError:
|
||
return 0
|
||
return 0
|
||
|
||
|
||
def _metadata_is_balance(payment: YooKassaPayment) -> bool:
|
||
metadata = getattr(payment, 'metadata_json', {}) or {}
|
||
payment_type = str(metadata.get('type') or metadata.get('payment_type') or '').lower()
|
||
return payment_type.startswith('balance_topup')
|
||
|
||
|
||
def _build_record(
|
||
method: PaymentMethod,
|
||
payment: Any,
|
||
*,
|
||
identifier: str,
|
||
amount_kopeks: int,
|
||
status: str,
|
||
is_paid: bool,
|
||
expires_at: datetime | None = None,
|
||
) -> PendingPayment | None:
|
||
user = getattr(payment, 'user', None)
|
||
if user is None:
|
||
logger.debug('Skipping payment without linked user', method_value=method.value, identifier=identifier)
|
||
return None
|
||
|
||
created_at = getattr(payment, 'created_at', None)
|
||
if not isinstance(created_at, datetime):
|
||
logger.debug('Skipping payment without valid created_at', method_value=method.value, identifier=identifier)
|
||
return None
|
||
|
||
local_id = getattr(payment, 'id', None)
|
||
if local_id is None:
|
||
logger.debug('Skipping payment without local id', method_value=method.value)
|
||
return None
|
||
|
||
return PendingPayment(
|
||
method=method,
|
||
local_id=int(local_id),
|
||
identifier=identifier,
|
||
amount_kopeks=amount_kopeks,
|
||
status=status,
|
||
is_paid=is_paid,
|
||
created_at=created_at,
|
||
user=user,
|
||
payment=payment,
|
||
expires_at=expires_at,
|
||
)
|
||
|
||
|
||
async def _fetch_pal24_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(Pal24Payment)
|
||
.options(selectinload(Pal24Payment.user))
|
||
.where(Pal24Payment.created_at >= cutoff)
|
||
.order_by(desc(Pal24Payment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_pal24_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.PAL24,
|
||
payment,
|
||
identifier=payment.bill_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_mulenpay_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(MulenPayPayment)
|
||
.options(selectinload(MulenPayPayment.user))
|
||
.where(MulenPayPayment.created_at >= cutoff)
|
||
.order_by(desc(MulenPayPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_mulenpay_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.MULENPAY,
|
||
payment,
|
||
identifier=payment.uuid,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_wata_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(WataPayment)
|
||
.options(selectinload(WataPayment.user))
|
||
.where(WataPayment.created_at >= cutoff)
|
||
.order_by(desc(WataPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_wata_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.WATA,
|
||
payment,
|
||
identifier=payment.payment_link_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_platega_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(PlategaPayment)
|
||
.options(selectinload(PlategaPayment.user))
|
||
.where(PlategaPayment.created_at >= cutoff)
|
||
.order_by(desc(PlategaPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_platega_pending(payment):
|
||
continue
|
||
identifier = payment.platega_transaction_id or payment.correlation_id or str(payment.id)
|
||
record = _build_record(
|
||
PaymentMethod.PLATEGA,
|
||
payment,
|
||
identifier=identifier,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_heleket_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(HeleketPayment)
|
||
.options(selectinload(HeleketPayment.user))
|
||
.where(HeleketPayment.created_at >= cutoff)
|
||
.order_by(desc(HeleketPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_heleket_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.HELEKET,
|
||
payment,
|
||
identifier=payment.uuid,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_yookassa_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(YooKassaPayment)
|
||
.options(selectinload(YooKassaPayment.user))
|
||
.where(YooKassaPayment.created_at >= cutoff)
|
||
.order_by(desc(YooKassaPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if payment.transaction_id:
|
||
continue
|
||
if not _metadata_is_balance(payment):
|
||
continue
|
||
if not _is_yookassa_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.YOOKASSA,
|
||
payment,
|
||
identifier=payment.yookassa_payment_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(getattr(payment, 'is_paid', False)),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_cryptobot_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(CryptoBotPayment)
|
||
.options(selectinload(CryptoBotPayment.user))
|
||
.where(CryptoBotPayment.created_at >= cutoff)
|
||
.order_by(desc(CryptoBotPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
status = (payment.status or '').lower()
|
||
if not _is_cryptobot_pending(payment) and status != 'paid':
|
||
continue
|
||
amount_kopeks = _parse_cryptobot_amount_kopeks(payment)
|
||
record = _build_record(
|
||
PaymentMethod.CRYPTOBOT,
|
||
payment,
|
||
identifier=payment.invoice_id,
|
||
amount_kopeks=amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_cloudpayments_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(CloudPaymentsPayment)
|
||
.options(selectinload(CloudPaymentsPayment.user))
|
||
.where(CloudPaymentsPayment.created_at >= cutoff)
|
||
.order_by(desc(CloudPaymentsPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_cloudpayments_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.CLOUDPAYMENTS,
|
||
payment,
|
||
identifier=payment.invoice_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_freekassa_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(FreekassaPayment)
|
||
.options(selectinload(FreekassaPayment.user))
|
||
.where(FreekassaPayment.created_at >= cutoff)
|
||
.order_by(desc(FreekassaPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_freekassa_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.FREEKASSA,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_kassa_ai_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(KassaAiPayment)
|
||
.options(selectinload(KassaAiPayment.user))
|
||
.where(KassaAiPayment.created_at >= cutoff)
|
||
.order_by(desc(KassaAiPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_kassa_ai_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.KASSA_AI,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_riopay_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(RioPayPayment)
|
||
.options(selectinload(RioPayPayment.user))
|
||
.where(RioPayPayment.created_at >= cutoff)
|
||
.order_by(desc(RioPayPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_riopay_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.RIOPAY,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_severpay_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(SeverPayPayment)
|
||
.options(selectinload(SeverPayPayment.user))
|
||
.where(SeverPayPayment.created_at >= cutoff)
|
||
.order_by(desc(SeverPayPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_severpay_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.SEVERPAY,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_paypear_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(PayPearPayment)
|
||
.options(selectinload(PayPearPayment.user))
|
||
.where(PayPearPayment.created_at >= cutoff)
|
||
.order_by(desc(PayPearPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_paypear_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.PAYPEAR,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_rollypay_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(RollyPayPayment)
|
||
.options(selectinload(RollyPayPayment.user))
|
||
.where(RollyPayPayment.created_at >= cutoff)
|
||
.order_by(desc(RollyPayPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_rollypay_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.ROLLYPAY,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_aurapay_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(AuraPayPayment)
|
||
.options(selectinload(AuraPayPayment.user))
|
||
.where(AuraPayPayment.created_at >= cutoff)
|
||
.order_by(desc(AuraPayPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_aurapay_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.AURAPAY,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_etoplatezhi_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(EtoplatezhiPayment)
|
||
.options(selectinload(EtoplatezhiPayment.user))
|
||
.where(EtoplatezhiPayment.created_at >= cutoff)
|
||
.order_by(desc(EtoplatezhiPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_etoplatezhi_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.ETOPLATEZHI,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_antilopay_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(AntilopayPayment)
|
||
.options(selectinload(AntilopayPayment.user))
|
||
.where(AntilopayPayment.created_at >= cutoff)
|
||
.order_by(desc(AntilopayPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_antilopay_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.ANTILOPAY,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_jupiter_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(JupiterPayment)
|
||
.options(selectinload(JupiterPayment.user))
|
||
.where(JupiterPayment.created_at >= cutoff)
|
||
.order_by(desc(JupiterPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_jupiter_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.JUPITER,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_donut_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(DonutPayment)
|
||
.options(selectinload(DonutPayment.user))
|
||
.where(DonutPayment.created_at >= cutoff)
|
||
.order_by(desc(DonutPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_donut_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.DONUT,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_lava_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(LavaPayment)
|
||
.options(selectinload(LavaPayment.user))
|
||
.where(LavaPayment.created_at >= cutoff)
|
||
.order_by(desc(LavaPayment.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for payment in result.scalars().all():
|
||
if not _is_lava_pending(payment):
|
||
continue
|
||
record = _build_record(
|
||
PaymentMethod.LAVA,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def _fetch_stars_transactions(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||
stmt = (
|
||
select(Transaction)
|
||
.options(selectinload(Transaction.user))
|
||
.where(
|
||
Transaction.created_at >= cutoff,
|
||
Transaction.type == TransactionType.DEPOSIT.value,
|
||
Transaction.payment_method == PaymentMethod.TELEGRAM_STARS.value,
|
||
)
|
||
.order_by(desc(Transaction.created_at))
|
||
)
|
||
result = await db.execute(stmt)
|
||
records: list[PendingPayment] = []
|
||
for transaction in result.scalars().all():
|
||
record = _build_record(
|
||
PaymentMethod.TELEGRAM_STARS,
|
||
transaction,
|
||
identifier=transaction.external_id or str(transaction.id),
|
||
amount_kopeks=transaction.amount_kopeks,
|
||
status='paid' if transaction.is_completed else 'pending',
|
||
is_paid=bool(transaction.is_completed),
|
||
)
|
||
if record:
|
||
records.append(record)
|
||
return records
|
||
|
||
|
||
async def list_recent_pending_payments(
|
||
db: AsyncSession,
|
||
*,
|
||
max_age: timedelta = PENDING_MAX_AGE,
|
||
) -> list[PendingPayment]:
|
||
"""Return pending payments (top-ups) from supported providers within the age window."""
|
||
|
||
cutoff = datetime.now(UTC) - max_age
|
||
|
||
tasks: Iterable[list[PendingPayment]] = (
|
||
await _fetch_yookassa_payments(db, cutoff),
|
||
await _fetch_pal24_payments(db, cutoff),
|
||
await _fetch_mulenpay_payments(db, cutoff),
|
||
await _fetch_wata_payments(db, cutoff),
|
||
await _fetch_platega_payments(db, cutoff),
|
||
await _fetch_heleket_payments(db, cutoff),
|
||
await _fetch_cryptobot_payments(db, cutoff),
|
||
await _fetch_cloudpayments_payments(db, cutoff),
|
||
await _fetch_freekassa_payments(db, cutoff),
|
||
await _fetch_kassa_ai_payments(db, cutoff),
|
||
await _fetch_riopay_payments(db, cutoff),
|
||
await _fetch_severpay_payments(db, cutoff),
|
||
await _fetch_paypear_payments(db, cutoff),
|
||
await _fetch_rollypay_payments(db, cutoff),
|
||
await _fetch_aurapay_payments(db, cutoff),
|
||
await _fetch_etoplatezhi_payments(db, cutoff),
|
||
await _fetch_antilopay_payments(db, cutoff),
|
||
await _fetch_jupiter_payments(db, cutoff),
|
||
await _fetch_donut_payments(db, cutoff),
|
||
await _fetch_lava_payments(db, cutoff),
|
||
await _fetch_stars_transactions(db, cutoff),
|
||
)
|
||
|
||
records: list[PendingPayment] = []
|
||
for batch in tasks:
|
||
records.extend(batch)
|
||
|
||
records.sort(key=lambda item: item.created_at, reverse=True)
|
||
return records
|
||
|
||
|
||
async def get_payment_record(
|
||
db: AsyncSession,
|
||
method: PaymentMethod,
|
||
local_payment_id: int,
|
||
) -> PendingPayment | None:
|
||
"""Load single payment record and normalize it to :class:`PendingPayment`."""
|
||
|
||
cutoff = datetime.now(UTC) - PENDING_MAX_AGE
|
||
|
||
if method == PaymentMethod.PAL24:
|
||
payment = await db.get(Pal24Payment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.bill_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.MULENPAY:
|
||
payment = await db.get(MulenPayPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.uuid,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
|
||
if method == PaymentMethod.WATA:
|
||
payment = await db.get(WataPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.payment_link_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.PLATEGA:
|
||
payment = await db.get(PlategaPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
identifier = payment.platega_transaction_id or payment.correlation_id or str(payment.id)
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=identifier,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.HELEKET:
|
||
payment = await db.get(HeleketPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.uuid,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.YOOKASSA:
|
||
payment = await db.get(YooKassaPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
if payment.created_at < cutoff:
|
||
logger.debug('YooKassa payment is older than cutoff', payment_id=payment.id)
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.yookassa_payment_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(getattr(payment, 'is_paid', False)),
|
||
)
|
||
|
||
if method == PaymentMethod.CRYPTOBOT:
|
||
payment = await db.get(CryptoBotPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
amount_kopeks = _parse_cryptobot_amount_kopeks(payment)
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.invoice_id,
|
||
amount_kopeks=amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
|
||
if method == PaymentMethod.CLOUDPAYMENTS:
|
||
payment = await db.get(CloudPaymentsPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.invoice_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
|
||
if method == PaymentMethod.FREEKASSA:
|
||
payment = await db.get(FreekassaPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
|
||
if method == PaymentMethod.KASSA_AI:
|
||
payment = await db.get(KassaAiPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
)
|
||
|
||
if method == PaymentMethod.RIOPAY:
|
||
payment = await db.get(RioPayPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.SEVERPAY:
|
||
payment = await db.get(SeverPayPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.PAYPEAR:
|
||
payment = await db.get(PayPearPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.ROLLYPAY:
|
||
payment = await db.get(RollyPayPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.AURAPAY:
|
||
payment = await db.get(AuraPayPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.ETOPLATEZHI:
|
||
payment = await db.get(EtoplatezhiPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.ANTILOPAY:
|
||
payment = await db.get(AntilopayPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.JUPITER:
|
||
payment = await db.get(JupiterPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.DONUT:
|
||
payment = await db.get(DonutPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.LAVA:
|
||
payment = await db.get(LavaPayment, local_payment_id)
|
||
if not payment:
|
||
return None
|
||
await db.refresh(payment, attribute_names=['user'])
|
||
return _build_record(
|
||
method,
|
||
payment,
|
||
identifier=payment.order_id,
|
||
amount_kopeks=payment.amount_kopeks,
|
||
status=payment.status or '',
|
||
is_paid=bool(payment.is_paid),
|
||
expires_at=getattr(payment, 'expires_at', None),
|
||
)
|
||
|
||
if method == PaymentMethod.TELEGRAM_STARS:
|
||
transaction = await db.get(Transaction, local_payment_id)
|
||
if not transaction:
|
||
return None
|
||
await db.refresh(transaction, attribute_names=['user'])
|
||
if transaction.payment_method != PaymentMethod.TELEGRAM_STARS.value:
|
||
return None
|
||
return _build_record(
|
||
method,
|
||
transaction,
|
||
identifier=transaction.external_id or str(transaction.id),
|
||
amount_kopeks=transaction.amount_kopeks,
|
||
status='paid' if transaction.is_completed else 'pending',
|
||
is_paid=bool(transaction.is_completed),
|
||
)
|
||
|
||
logger.debug('Unsupported payment method requested', method=method)
|
||
return None
|
||
|
||
|
||
async def run_manual_check(
|
||
db: AsyncSession,
|
||
method: PaymentMethod,
|
||
local_payment_id: int,
|
||
payment_service: PaymentService,
|
||
) -> PendingPayment | None:
|
||
"""Trigger provider specific status refresh and return the updated record."""
|
||
|
||
try:
|
||
if method == PaymentMethod.PAL24:
|
||
result = await payment_service.get_pal24_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.MULENPAY:
|
||
result = await payment_service.get_mulenpay_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.WATA:
|
||
result = await payment_service.get_wata_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.PLATEGA:
|
||
result = await payment_service.get_platega_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.HELEKET:
|
||
payment = await payment_service.sync_heleket_payment_status(db, local_payment_id=local_payment_id)
|
||
elif method == PaymentMethod.YOOKASSA:
|
||
result = await payment_service.get_yookassa_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.CRYPTOBOT:
|
||
result = await payment_service.get_cryptobot_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.CLOUDPAYMENTS:
|
||
result = await payment_service.get_cloudpayments_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.FREEKASSA:
|
||
result = await payment_service.get_freekassa_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.KASSA_AI:
|
||
result = await payment_service.get_kassa_ai_payment_status(db, local_payment_id)
|
||
payment = result.get('payment') if result else None
|
||
elif method == PaymentMethod.SEVERPAY:
|
||
severpay_payment = await db.get(SeverPayPayment, local_payment_id)
|
||
if severpay_payment:
|
||
result = await payment_service.check_severpay_payment_status(db, severpay_payment.order_id)
|
||
payment = result.get('payment') if result else None
|
||
else:
|
||
payment = None
|
||
elif method == PaymentMethod.RIOPAY:
|
||
riopay_payment = await db.get(RioPayPayment, local_payment_id)
|
||
if riopay_payment:
|
||
result = await payment_service.check_riopay_payment_status(db, riopay_payment.order_id)
|
||
payment = result.get('payment') if result else None
|
||
else:
|
||
payment = None
|
||
elif method == PaymentMethod.PAYPEAR:
|
||
paypear_payment = await db.get(PayPearPayment, local_payment_id)
|
||
if paypear_payment:
|
||
result = await payment_service.check_paypear_payment_status(db, paypear_payment.order_id)
|
||
payment = result.get('payment') if result else None
|
||
else:
|
||
payment = None
|
||
elif method == PaymentMethod.ROLLYPAY:
|
||
rollypay_payment = await db.get(RollyPayPayment, local_payment_id)
|
||
if rollypay_payment:
|
||
result = await payment_service.check_rollypay_payment_status(db, rollypay_payment.order_id)
|
||
payment = result.get('payment') if result else None
|
||
else:
|
||
payment = None
|
||
elif method == PaymentMethod.AURAPAY:
|
||
aurapay_payment = await db.get(AuraPayPayment, local_payment_id)
|
||
if aurapay_payment:
|
||
result = await payment_service.check_aurapay_payment_status(db, aurapay_payment.order_id)
|
||
payment = result.get('payment') if result else None
|
||
else:
|
||
payment = None
|
||
else:
|
||
logger.warning('Manual check requested for unsupported method', method=method)
|
||
return None
|
||
|
||
if not payment:
|
||
return None
|
||
|
||
return await get_payment_record(db, method, local_payment_id)
|
||
|
||
except Exception as error: # pragma: no cover - defensive logging
|
||
logger.error(
|
||
'Manual status check failed for payment',
|
||
method_value=method.value,
|
||
local_payment_id=local_payment_id,
|
||
error=error,
|
||
exc_info=True,
|
||
)
|
||
# Откатываем сессию чтобы не оставлять её в грязном состоянии
|
||
if db.in_transaction():
|
||
await db.rollback()
|
||
return None
|
||
|
||
|
||
if TYPE_CHECKING: # pragma: no cover
|
||
from app.services.payment_service import PaymentService
|