""" Coins Service - handles all coin-related operations Coins are earned only in certified marathons and can be spent in the shop. """ from sqlalchemy.ext.asyncio import AsyncSession from app.models import User, Participant, Marathon, CoinTransaction, CoinTransactionType from app.models.challenge import Difficulty class CoinsService: """Service for managing coin transactions and balances""" # Coins awarded per challenge difficulty (only in certified marathons) CHALLENGE_COINS = { Difficulty.EASY.value: 5, Difficulty.MEDIUM.value: 12, Difficulty.HARD.value: 25, } # Coins for playthrough = points * this ratio PLAYTHROUGH_COIN_RATIO = 0.05 # 5% of points # Coins awarded for marathon placements MARATHON_PLACE_COINS = { 1: 100, # 1st place 2: 50, # 2nd place 3: 30, # 3rd place } # Bonus coins for Common Enemy event winners COMMON_ENEMY_BONUS_COINS = { 1: 15, # First to complete 2: 10, # Second 3: 5, # Third } async def award_challenge_coins( self, db: AsyncSession, user: User, participant: Participant, marathon: Marathon, difficulty: str, assignment_id: int, ) -> int: """ Award coins for completing a challenge. Only awards coins if marathon is certified. Returns: number of coins awarded (0 if marathon not certified) """ if not marathon.is_certified: return 0 coins = self.CHALLENGE_COINS.get(difficulty, 0) if coins <= 0: return 0 # Create transaction transaction = CoinTransaction( user_id=user.id, amount=coins, transaction_type=CoinTransactionType.CHALLENGE_COMPLETE.value, reference_type="assignment", reference_id=assignment_id, description=f"Challenge completion ({difficulty})", ) db.add(transaction) # Update balances user.coins_balance += coins participant.coins_earned += coins return coins async def award_playthrough_coins( self, db: AsyncSession, user: User, participant: Participant, marathon: Marathon, points: int, assignment_id: int, ) -> int: """ Award coins for completing a playthrough. Coins = points * PLAYTHROUGH_COIN_RATIO Returns: number of coins awarded (0 if marathon not certified) """ if not marathon.is_certified: return 0 coins = int(points * self.PLAYTHROUGH_COIN_RATIO) if coins <= 0: return 0 transaction = CoinTransaction( user_id=user.id, amount=coins, transaction_type=CoinTransactionType.PLAYTHROUGH_COMPLETE.value, reference_type="assignment", reference_id=assignment_id, description=f"Playthrough completion ({points} points)", ) db.add(transaction) user.coins_balance += coins participant.coins_earned += coins return coins async def award_marathon_place( self, db: AsyncSession, user: User, marathon: Marathon, place: int, ) -> int: """ Award coins for placing in a marathon (1st, 2nd, 3rd). Returns: number of coins awarded (0 if not top 3 or not certified) """ if not marathon.is_certified: return 0 coins = self.MARATHON_PLACE_COINS.get(place, 0) if coins <= 0: return 0 transaction = CoinTransaction( user_id=user.id, amount=coins, transaction_type=CoinTransactionType.MARATHON_PLACE.value, reference_type="marathon", reference_id=marathon.id, description=f"Marathon #{place} place: {marathon.title}", ) db.add(transaction) user.coins_balance += coins return coins async def award_common_enemy_bonus( self, db: AsyncSession, user: User, participant: Participant, marathon: Marathon, rank: int, event_id: int, ) -> int: """ Award bonus coins for Common Enemy event completion. Returns: number of bonus coins awarded """ if not marathon.is_certified: return 0 coins = self.COMMON_ENEMY_BONUS_COINS.get(rank, 0) if coins <= 0: return 0 transaction = CoinTransaction( user_id=user.id, amount=coins, transaction_type=CoinTransactionType.COMMON_ENEMY_BONUS.value, reference_type="event", reference_id=event_id, description=f"Common Enemy #{rank} place", ) db.add(transaction) user.coins_balance += coins participant.coins_earned += coins return coins async def spend_coins( self, db: AsyncSession, user: User, amount: int, description: str, reference_type: str | None = None, reference_id: int | None = None, ) -> bool: """ Spend coins (for purchases). Returns: True if successful, False if insufficient balance """ if user.coins_balance < amount: return False transaction = CoinTransaction( user_id=user.id, amount=-amount, # Negative for spending transaction_type=CoinTransactionType.PURCHASE.value, reference_type=reference_type, reference_id=reference_id, description=description, ) db.add(transaction) user.coins_balance -= amount return True async def refund_coins( self, db: AsyncSession, user: User, amount: int, description: str, reference_type: str | None = None, reference_id: int | None = None, ) -> None: """Refund coins to user (for failed purchases, etc.)""" transaction = CoinTransaction( user_id=user.id, amount=amount, transaction_type=CoinTransactionType.REFUND.value, reference_type=reference_type, reference_id=reference_id, description=description, ) db.add(transaction) user.coins_balance += amount async def admin_grant_coins( self, db: AsyncSession, user: User, amount: int, reason: str, admin_id: int, ) -> None: """Admin grants coins to user""" transaction = CoinTransaction( user_id=user.id, amount=amount, transaction_type=CoinTransactionType.ADMIN_GRANT.value, reference_type="admin", reference_id=admin_id, description=f"Admin grant: {reason}", ) db.add(transaction) user.coins_balance += amount async def admin_deduct_coins( self, db: AsyncSession, user: User, amount: int, reason: str, admin_id: int, ) -> bool: """ Admin deducts coins from user. Returns: True if successful, False if insufficient balance """ if user.coins_balance < amount: return False transaction = CoinTransaction( user_id=user.id, amount=-amount, transaction_type=CoinTransactionType.ADMIN_DEDUCT.value, reference_type="admin", reference_id=admin_id, description=f"Admin deduction: {reason}", ) db.add(transaction) user.coins_balance -= amount return True # Singleton instance coins_service = CoinsService()