import logging from typing import Optional, List from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, and_ from sqlalchemy.orm import selectinload from app.database.models import YooKassaPayment, User, Transaction logger = logging.getLogger(__name__) async def create_yookassa_payment( db: AsyncSession, user_id: int, yookassa_payment_id: str, amount_kopeks: int, currency: str, description: str, status: str, confirmation_url: Optional[str] = None, metadata_json: Optional[dict] = None, payment_method_type: Optional[str] = None, yookassa_created_at: Optional[datetime] = None, test_mode: bool = False ) -> YooKassaPayment: payment = YooKassaPayment( user_id=user_id, yookassa_payment_id=yookassa_payment_id, amount_kopeks=amount_kopeks, currency=currency, description=description, status=status, confirmation_url=confirmation_url, metadata_json=metadata_json, payment_method_type=payment_method_type, yookassa_created_at=yookassa_created_at, test_mode=test_mode ) db.add(payment) await db.commit() await db.refresh(payment) logger.info(f"Создан платеж YooKassa: {yookassa_payment_id} на {amount_kopeks/100}₽ для пользователя {user_id}") return payment async def get_yookassa_payment_by_id( db: AsyncSession, yookassa_payment_id: str ) -> Optional[YooKassaPayment]: result = await db.execute( select(YooKassaPayment) .options(selectinload(YooKassaPayment.user)) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) ) return result.scalar_one_or_none() async def get_yookassa_payment_by_local_id( db: AsyncSession, local_id: int ) -> Optional[YooKassaPayment]: result = await db.execute( select(YooKassaPayment) .options(selectinload(YooKassaPayment.user)) .where(YooKassaPayment.id == local_id) ) return result.scalar_one_or_none() async def update_yookassa_payment_status( db: AsyncSession, yookassa_payment_id: str, status: str, is_paid: bool = False, is_captured: bool = False, captured_at: Optional[datetime] = None, payment_method_type: Optional[str] = None ) -> Optional[YooKassaPayment]: update_data = { "status": status, "is_paid": is_paid, "is_captured": is_captured, "updated_at": datetime.utcnow() } if captured_at: update_data["captured_at"] = captured_at if payment_method_type: update_data["payment_method_type"] = payment_method_type await db.execute( update(YooKassaPayment) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) .values(**update_data) ) await db.commit() result = await db.execute( select(YooKassaPayment) .options(selectinload(YooKassaPayment.user)) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) ) payment = result.scalar_one_or_none() if payment: logger.info(f"Обновлен статус платежа YooKassa {yookassa_payment_id}: {status}, paid={is_paid}") return payment async def link_yookassa_payment_to_transaction( db: AsyncSession, yookassa_payment_id: str, transaction_id: int ) -> Optional[YooKassaPayment]: await db.execute( update(YooKassaPayment) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) .values(transaction_id=transaction_id, updated_at=datetime.utcnow()) ) await db.commit() result = await db.execute( select(YooKassaPayment) .options(selectinload(YooKassaPayment.user), selectinload(YooKassaPayment.transaction)) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) ) payment = result.scalar_one_or_none() if payment: logger.info(f"Платеж YooKassa {yookassa_payment_id} связан с транзакцией {transaction_id}") return payment async def get_user_yookassa_payments( db: AsyncSession, user_id: int, limit: int = 50, offset: int = 0 ) -> List[YooKassaPayment]: result = await db.execute( select(YooKassaPayment) .options(selectinload(YooKassaPayment.transaction)) .where(YooKassaPayment.user_id == user_id) .order_by(YooKassaPayment.created_at.desc()) .limit(limit) .offset(offset) ) return result.scalars().all() async def get_pending_yookassa_payments( db: AsyncSession, user_id: Optional[int] = None, limit: int = 100 ) -> List[YooKassaPayment]: query = select(YooKassaPayment).options(selectinload(YooKassaPayment.user)) conditions = [YooKassaPayment.status.in_(["pending", "waiting_for_capture"])] if user_id: conditions.append(YooKassaPayment.user_id == user_id) result = await db.execute( query.where(and_(*conditions)) .order_by(YooKassaPayment.created_at.desc()) .limit(limit) ) return result.scalars().all() async def get_succeeded_yookassa_payments_without_transaction( db: AsyncSession, limit: int = 50 ) -> List[YooKassaPayment]: result = await db.execute( select(YooKassaPayment) .options(selectinload(YooKassaPayment.user)) .where( and_( YooKassaPayment.status == "succeeded", YooKassaPayment.is_paid == True, YooKassaPayment.transaction_id == None ) ) .order_by(YooKassaPayment.captured_at.desc()) .limit(limit) ) return result.scalars().all() async def delete_yookassa_payment( db: AsyncSession, yookassa_payment_id: str ) -> bool: result = await db.execute( select(YooKassaPayment) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) ) payment = result.scalar_one_or_none() if payment: await db.delete(payment) await db.commit() logger.info(f"Удален платеж YooKassa: {yookassa_payment_id}") return True return False async def get_yookassa_payments_stats( db: AsyncSession, user_id: Optional[int] = None ) -> dict: from sqlalchemy import func, case query = select( func.count(YooKassaPayment.id).label('total_payments'), func.sum(YooKassaPayment.amount_kopeks).label('total_amount_kopeks'), func.sum( case( (YooKassaPayment.status == 'succeeded', YooKassaPayment.amount_kopeks), else_=0 ) ).label('succeeded_amount_kopeks'), func.count( case( (YooKassaPayment.status == 'succeeded', 1), else_=None ) ).label('succeeded_count'), func.count( case( (YooKassaPayment.status == 'pending', 1), else_=None ) ).label('pending_count'), func.count( case( (YooKassaPayment.status.in_(['canceled', 'failed']), 1), else_=None ) ).label('failed_count') ).select_from(YooKassaPayment) if user_id: query = query.where(YooKassaPayment.user_id == user_id) result = await db.execute(query) stats = result.first() return { 'total_payments': stats.total_payments or 0, 'total_amount_kopeks': stats.total_amount_kopeks or 0, 'total_amount_rubles': (stats.total_amount_kopeks or 0) / 100, 'succeeded_amount_kopeks': stats.succeeded_amount_kopeks or 0, 'succeeded_amount_rubles': (stats.succeeded_amount_kopeks or 0) / 100, 'succeeded_count': stats.succeeded_count or 0, 'pending_count': stats.pending_count or 0, 'failed_count': stats.failed_count or 0, 'success_rate': (stats.succeeded_count / stats.total_payments * 100) if stats.total_payments > 0 else 0 }