277 lines
8.0 KiB
Python
277 lines
8.0 KiB
Python
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
|
|
}
|