Add shop
This commit is contained in:
288
backend/app/services/coins.py
Normal file
288
backend/app/services/coins.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user