- Интеграция рекурентов от Юкассы
- Багфикс личного кабинета
This commit is contained in:
@@ -50,6 +50,20 @@ async def create_referral_earning(
|
||||
return earning
|
||||
|
||||
|
||||
async def get_commission_payment_count(db: AsyncSession, referrer_id: int, referral_id: int) -> int:
|
||||
"""Подсчитать количество комиссионных начислений реферера за платежи конкретного реферала."""
|
||||
result = await db.execute(
|
||||
select(func.count(ReferralEarning.id)).where(
|
||||
and_(
|
||||
ReferralEarning.user_id == referrer_id,
|
||||
ReferralEarning.referral_id == referral_id,
|
||||
ReferralEarning.reason.in_(['referral_commission_topup', 'referral_first_topup']),
|
||||
)
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
|
||||
async def get_referral_earnings_by_user(
|
||||
db: AsyncSession, user_id: int, limit: int = 50, offset: int = 0
|
||||
) -> list[ReferralEarning]:
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import SavedPaymentMethod
|
||||
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def create_saved_payment_method(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
yookassa_payment_method_id: str,
|
||||
method_type: str = 'bank_card',
|
||||
card_first6: str | None = None,
|
||||
card_last4: str | None = None,
|
||||
card_type: str | None = None,
|
||||
card_expiry_month: str | None = None,
|
||||
card_expiry_year: str | None = None,
|
||||
title: str | None = None,
|
||||
) -> SavedPaymentMethod | None:
|
||||
"""Создаёт или реактивирует сохранённый метод оплаты."""
|
||||
|
||||
# Проверяем, есть ли уже такой метод (включая деактивированные)
|
||||
existing = await get_payment_method_by_yookassa_id(db, yookassa_payment_method_id, include_inactive=True)
|
||||
if existing:
|
||||
# Реактивируем и обновляем данные
|
||||
existing.is_active = True
|
||||
existing.method_type = method_type
|
||||
existing.card_first6 = card_first6
|
||||
existing.card_last4 = card_last4
|
||||
existing.card_type = card_type
|
||||
existing.card_expiry_month = card_expiry_month
|
||||
existing.card_expiry_year = card_expiry_year
|
||||
existing.title = title
|
||||
existing.updated_at = datetime.now(UTC)
|
||||
await db.commit()
|
||||
await db.refresh(existing)
|
||||
logger.info(
|
||||
'Реактивирован сохранённый метод оплаты',
|
||||
saved_method_id=existing.id,
|
||||
user_id=user_id,
|
||||
method_type=method_type,
|
||||
card_last4=card_last4,
|
||||
)
|
||||
return existing
|
||||
|
||||
method = SavedPaymentMethod(
|
||||
user_id=user_id,
|
||||
yookassa_payment_method_id=yookassa_payment_method_id,
|
||||
method_type=method_type,
|
||||
card_first6=card_first6,
|
||||
card_last4=card_last4,
|
||||
card_type=card_type,
|
||||
card_expiry_month=card_expiry_month,
|
||||
card_expiry_year=card_expiry_year,
|
||||
title=title,
|
||||
)
|
||||
|
||||
db.add(method)
|
||||
try:
|
||||
await db.commit()
|
||||
except IntegrityError as e:
|
||||
await db.rollback()
|
||||
logger.error(
|
||||
'Ошибка создания сохранённого метода оплаты',
|
||||
yookassa_payment_method_id=yookassa_payment_method_id,
|
||||
user_id=user_id,
|
||||
e=e,
|
||||
)
|
||||
return None
|
||||
await db.refresh(method)
|
||||
|
||||
logger.info(
|
||||
'Создан сохранённый метод оплаты',
|
||||
saved_method_id=method.id,
|
||||
user_id=user_id,
|
||||
method_type=method_type,
|
||||
card_last4=card_last4,
|
||||
)
|
||||
return method
|
||||
|
||||
|
||||
async def get_active_payment_methods_by_user(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
) -> list[SavedPaymentMethod]:
|
||||
"""Получить все активные сохранённые методы оплаты пользователя."""
|
||||
result = await db.execute(
|
||||
select(SavedPaymentMethod)
|
||||
.where(
|
||||
SavedPaymentMethod.user_id == user_id,
|
||||
SavedPaymentMethod.is_active == True,
|
||||
)
|
||||
.order_by(SavedPaymentMethod.created_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_payment_method_by_yookassa_id(
|
||||
db: AsyncSession,
|
||||
yookassa_payment_method_id: str,
|
||||
include_inactive: bool = False,
|
||||
) -> SavedPaymentMethod | None:
|
||||
"""Найти сохранённый метод по YooKassa payment_method.id."""
|
||||
query = select(SavedPaymentMethod).where(
|
||||
SavedPaymentMethod.yookassa_payment_method_id == yookassa_payment_method_id,
|
||||
)
|
||||
if not include_inactive:
|
||||
query = query.where(SavedPaymentMethod.is_active == True)
|
||||
result = await db.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def deactivate_payment_method(
|
||||
db: AsyncSession,
|
||||
saved_method_id: int,
|
||||
user_id: int,
|
||||
) -> bool:
|
||||
"""Деактивировать (soft-delete) сохранённый метод оплаты."""
|
||||
result = await db.execute(
|
||||
update(SavedPaymentMethod)
|
||||
.where(
|
||||
SavedPaymentMethod.id == saved_method_id,
|
||||
SavedPaymentMethod.user_id == user_id,
|
||||
SavedPaymentMethod.is_active == True,
|
||||
)
|
||||
.values(is_active=False, updated_at=datetime.now(UTC))
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
if result.rowcount > 0:
|
||||
logger.info(
|
||||
'Метод оплаты деактивирован',
|
||||
saved_method_id=saved_method_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def deactivate_all_user_payment_methods(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
) -> int:
|
||||
"""Деактивировать все методы оплаты пользователя. Возвращает количество деактивированных."""
|
||||
result = await db.execute(
|
||||
update(SavedPaymentMethod)
|
||||
.where(
|
||||
SavedPaymentMethod.user_id == user_id,
|
||||
SavedPaymentMethod.is_active == True,
|
||||
)
|
||||
.values(is_active=False, updated_at=datetime.now(UTC))
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
if result.rowcount > 0:
|
||||
logger.info(
|
||||
'Все методы оплаты пользователя деактивированы',
|
||||
user_id=user_id,
|
||||
count=result.rowcount,
|
||||
)
|
||||
return result.rowcount
|
||||
@@ -236,6 +236,37 @@ class YooKassaPayment(Base):
|
||||
return f'<YooKassaPayment(id={self.id}, yookassa_id={self.yookassa_payment_id}, amount={self.amount_rubles}₽, status={self.status})>'
|
||||
|
||||
|
||||
class SavedPaymentMethod(Base):
|
||||
__tablename__ = 'saved_payment_methods'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True)
|
||||
|
||||
# YooKassa payment_method.id — ключ для рекуррентных списаний
|
||||
yookassa_payment_method_id = Column(String(255), unique=True, nullable=False, index=True)
|
||||
|
||||
# Тип метода: bank_card, yoo_money, sberbank, tinkoff_bank, sbp, mir_pay
|
||||
method_type = Column(String(50), nullable=False, default='bank_card')
|
||||
|
||||
# Отображаемые данные карты (маскированные)
|
||||
card_first6 = Column(String(6), nullable=True)
|
||||
card_last4 = Column(String(4), nullable=True)
|
||||
card_type = Column(String(50), nullable=True) # Visa, MasterCard, Mir
|
||||
card_expiry_month = Column(String(2), nullable=True)
|
||||
card_expiry_year = Column(String(4), nullable=True)
|
||||
title = Column(String(255), nullable=True) # "Bank card *4444"
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
created_at = Column(AwareDateTime(), default=func.now())
|
||||
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
|
||||
|
||||
user = relationship('User', backref='saved_payment_methods')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SavedPaymentMethod(id={self.id}, user_id={self.user_id}, type={self.method_type}, last4={self.card_last4})>'
|
||||
|
||||
|
||||
class CryptoBotPayment(Base):
|
||||
__tablename__ = 'cryptobot_payments'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user