""" CRUD операции для колеса удачи (Fortune Wheel). """ import logging from datetime import datetime from typing import Any from sqlalchemy import and_, desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database.models import ( WheelConfig, WheelPrize, WheelSpin, ) logger = logging.getLogger(__name__) # ==================== WHEEL CONFIG ==================== async def get_wheel_config(db: AsyncSession) -> WheelConfig | None: """Получить текущую конфигурацию колеса (всегда id=1).""" result = await db.execute(select(WheelConfig).options(selectinload(WheelConfig.prizes)).where(WheelConfig.id == 1)) return result.scalar_one_or_none() async def get_or_create_wheel_config(db: AsyncSession) -> WheelConfig: """Получить или создать конфигурацию колеса.""" config = await get_wheel_config(db) if config: return config # Создаем дефолтную конфигурацию config = WheelConfig( id=1, is_enabled=False, name='Колесо удачи', spin_cost_stars=10, spin_cost_days=1, spin_cost_stars_enabled=True, spin_cost_days_enabled=True, rtp_percent=80, daily_spin_limit=5, min_subscription_days_for_day_payment=3, promo_prefix='WHEEL', promo_validity_days=7, ) db.add(config) await db.commit() await db.refresh(config) logger.info('🎡 Создана дефолтная конфигурация колеса удачи') return config async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig: """Обновить конфигурацию колеса.""" config = await get_or_create_wheel_config(db) for key, value in kwargs.items(): if hasattr(config, key) and value is not None: setattr(config, key, value) config.updated_at = datetime.utcnow() await db.commit() await db.refresh(config) logger.info(f'🎡 Обновлена конфигурация колеса: {kwargs}') return config # ==================== WHEEL PRIZES ==================== async def get_wheel_prizes(db: AsyncSession, config_id: int = 1, active_only: bool = True) -> list[WheelPrize]: """Получить список призов колеса.""" query = select(WheelPrize).where(WheelPrize.config_id == config_id) if active_only: query = query.where(WheelPrize.is_active == True) query = query.order_by(WheelPrize.sort_order) result = await db.execute(query) return list(result.scalars().all()) async def get_wheel_prize_by_id(db: AsyncSession, prize_id: int) -> WheelPrize | None: """Получить приз по ID.""" result = await db.execute(select(WheelPrize).where(WheelPrize.id == prize_id)) return result.scalar_one_or_none() async def create_wheel_prize( db: AsyncSession, config_id: int, prize_type: str, prize_value: int, display_name: str, prize_value_kopeks: int, emoji: str = '🎁', color: str = '#3B82F6', sort_order: int = 0, manual_probability: float | None = None, is_active: bool = True, promo_balance_bonus_kopeks: int = 0, promo_subscription_days: int = 0, promo_traffic_gb: int = 0, ) -> WheelPrize: """Создать новый приз на колесе.""" prize = WheelPrize( config_id=config_id, prize_type=prize_type, prize_value=prize_value, display_name=display_name, prize_value_kopeks=prize_value_kopeks, emoji=emoji, color=color, sort_order=sort_order, manual_probability=manual_probability, is_active=is_active, promo_balance_bonus_kopeks=promo_balance_bonus_kopeks, promo_subscription_days=promo_subscription_days, promo_traffic_gb=promo_traffic_gb, ) db.add(prize) await db.commit() await db.refresh(prize) logger.info(f'🎁 Создан приз колеса: {display_name} ({prize_type})') return prize async def update_wheel_prize(db: AsyncSession, prize_id: int, **kwargs) -> WheelPrize | None: """Обновить приз колеса.""" prize = await get_wheel_prize_by_id(db, prize_id) if not prize: return None for key, value in kwargs.items(): if hasattr(prize, key) and value is not None: setattr(prize, key, value) prize.updated_at = datetime.utcnow() await db.commit() await db.refresh(prize) logger.info(f'🎁 Обновлен приз колеса ID={prize_id}: {kwargs}') return prize async def delete_wheel_prize(db: AsyncSession, prize_id: int) -> bool: """Удалить приз колеса.""" prize = await get_wheel_prize_by_id(db, prize_id) if not prize: return False await db.delete(prize) await db.commit() logger.info(f'🗑️ Удален приз колеса ID={prize_id}') return True async def reorder_wheel_prizes(db: AsyncSession, prize_ids: list[int]) -> bool: """Переупорядочить призы колеса.""" for index, prize_id in enumerate(prize_ids): prize = await get_wheel_prize_by_id(db, prize_id) if prize: prize.sort_order = index await db.commit() logger.info(f'🔄 Переупорядочены призы колеса: {prize_ids}') return True # ==================== WHEEL SPINS ==================== async def create_wheel_spin( db: AsyncSession, user_id: int, prize_id: int, payment_type: str, payment_amount: int, payment_value_kopeks: int, prize_type: str, prize_value: int, prize_display_name: str, prize_value_kopeks: int, generated_promocode_id: int | None = None, is_applied: bool = False, ) -> WheelSpin: """Создать запись о спине колеса.""" spin = WheelSpin( user_id=user_id, prize_id=prize_id, payment_type=payment_type, payment_amount=payment_amount, payment_value_kopeks=payment_value_kopeks, prize_type=prize_type, prize_value=prize_value, prize_display_name=prize_display_name, prize_value_kopeks=prize_value_kopeks, generated_promocode_id=generated_promocode_id, is_applied=is_applied, applied_at=datetime.utcnow() if is_applied else None, ) db.add(spin) await db.commit() await db.refresh(spin) logger.info(f"🎰 Создан спин колеса: user_id={user_id}, prize='{prize_display_name}'") return spin async def mark_spin_applied(db: AsyncSession, spin_id: int) -> WheelSpin | None: """Отметить спин как примененный.""" result = await db.execute(select(WheelSpin).where(WheelSpin.id == spin_id)) spin = result.scalar_one_or_none() if spin: spin.is_applied = True spin.applied_at = datetime.utcnow() await db.commit() await db.refresh(spin) return spin async def get_user_spins_today(db: AsyncSession, user_id: int) -> int: """Получить количество спинов пользователя за сегодня.""" today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) result = await db.execute( select(func.count(WheelSpin.id)).where( and_( WheelSpin.user_id == user_id, WheelSpin.created_at >= today_start, ) ) ) return result.scalar() or 0 async def get_user_spin_history( db: AsyncSession, user_id: int, limit: int = 20, offset: int = 0 ) -> tuple[list[WheelSpin], int]: """Получить историю спинов пользователя.""" # Общее количество count_result = await db.execute(select(func.count(WheelSpin.id)).where(WheelSpin.user_id == user_id)) total = count_result.scalar() or 0 # Спины с пагинацией (eager load prize relationship) result = await db.execute( select(WheelSpin) .options(selectinload(WheelSpin.prize)) .where(WheelSpin.user_id == user_id) .order_by(desc(WheelSpin.created_at)) .limit(limit) .offset(offset) ) spins = list(result.scalars().all()) return spins, total async def get_all_spins( db: AsyncSession, user_id: int | None = None, date_from: datetime | None = None, date_to: datetime | None = None, limit: int = 50, offset: int = 0, ) -> tuple[list[WheelSpin], int]: """Получить все спины с фильтрами (для админки).""" conditions = [] if user_id: conditions.append(WheelSpin.user_id == user_id) if date_from: conditions.append(WheelSpin.created_at >= date_from) if date_to: conditions.append(WheelSpin.created_at <= date_to) # Общее количество count_query = select(func.count(WheelSpin.id)) if conditions: count_query = count_query.where(and_(*conditions)) count_result = await db.execute(count_query) total = count_result.scalar() or 0 # Спины с пагинацией query = select(WheelSpin).options(selectinload(WheelSpin.user)) if conditions: query = query.where(and_(*conditions)) query = query.order_by(desc(WheelSpin.created_at)).limit(limit).offset(offset) result = await db.execute(query) spins = list(result.scalars().all()) return spins, total # ==================== STATISTICS ==================== async def get_wheel_statistics( db: AsyncSession, date_from: datetime | None = None, date_to: datetime | None = None ) -> dict[str, Any]: """Получить статистику колеса удачи.""" conditions = [] if date_from: conditions.append(WheelSpin.created_at >= date_from) if date_to: conditions.append(WheelSpin.created_at <= date_to) base_query = select(WheelSpin) if conditions: base_query = base_query.where(and_(*conditions)) # Общие метрики result = await db.execute( select( func.count(WheelSpin.id).label('total_spins'), func.coalesce(func.sum(WheelSpin.payment_value_kopeks), 0).label('total_revenue'), func.coalesce(func.sum(WheelSpin.prize_value_kopeks), 0).label('total_payout'), ).where(and_(*conditions) if conditions else True) ) row = result.one() total_spins = row.total_spins or 0 total_revenue = row.total_revenue or 0 total_payout = row.total_payout or 0 # Фактический RTP actual_rtp = (total_payout / total_revenue * 100) if total_revenue > 0 else 0 # Распределение по типу оплаты payment_dist = await db.execute( select( WheelSpin.payment_type, func.count(WheelSpin.id).label('count'), func.sum(WheelSpin.payment_value_kopeks).label('total'), ) .where(and_(*conditions) if conditions else True) .group_by(WheelSpin.payment_type) ) spins_by_payment_type = { row.payment_type: {'count': row.count, 'total_kopeks': row.total or 0} for row in payment_dist } # Распределение призов prizes_dist = await db.execute( select( WheelSpin.prize_type, WheelSpin.prize_display_name, func.count(WheelSpin.id).label('count'), func.sum(WheelSpin.prize_value_kopeks).label('total'), ) .where(and_(*conditions) if conditions else True) .group_by(WheelSpin.prize_type, WheelSpin.prize_display_name) ) prizes_distribution = [ { 'prize_type': row.prize_type, 'display_name': row.prize_display_name, 'count': row.count, 'total_kopeks': row.total or 0, } for row in prizes_dist ] # Топ выигрышей top_wins_result = await db.execute( select(WheelSpin) .options(selectinload(WheelSpin.user)) .where(and_(*conditions) if conditions else True) .where(WheelSpin.prize_value_kopeks > 0) .order_by(desc(WheelSpin.prize_value_kopeks)) .limit(10) ) top_wins = [ { 'user_id': spin.user_id, 'username': spin.user.username if spin.user else None, 'prize_display_name': spin.prize_display_name, 'prize_value_kopeks': spin.prize_value_kopeks, 'created_at': spin.created_at.isoformat() if spin.created_at else None, } for spin in top_wins_result.scalars().all() ] # Конфигурация для сравнения config = await get_wheel_config(db) configured_rtp = config.rtp_percent if config else 80 return { 'total_spins': total_spins, 'total_revenue_kopeks': total_revenue, 'total_payout_kopeks': total_payout, 'actual_rtp_percent': round(actual_rtp, 2), 'configured_rtp_percent': configured_rtp, 'spins_by_payment_type': spins_by_payment_type, 'prizes_distribution': prizes_distribution, 'top_wins': top_wins, 'period_from': date_from.isoformat() if date_from else None, 'period_to': date_to.isoformat() if date_to else None, }