289 lines
7.7 KiB
Python
289 lines
7.7 KiB
Python
"""
|
|
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: 10,
|
|
Difficulty.MEDIUM.value: 20,
|
|
Difficulty.HARD.value: 35,
|
|
}
|
|
|
|
# Coins for playthrough = points * this ratio
|
|
PLAYTHROUGH_COIN_RATIO = 0.10 # 10% of points
|
|
|
|
# Coins awarded for marathon placements
|
|
MARATHON_PLACE_COINS = {
|
|
1: 500, # 1st place
|
|
2: 250, # 2nd place
|
|
3: 150, # 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()
|