Files
mtgonline/backend/app/services/deck_manager.py
T
akadmin 1e7c762452 Complete Phase 2: Card import, deck building, and full API
- Add card import feature with fuzzy matching
- Implement deck CRUD and management endpoints
- Add user data APIs for groups, networks, preferences, activity, replays
- Create comprehensive API documentation (API_DOCUMENTATION.md)
- Add ENDPOINT_AUDIT.md for endpoint verification
- Update documentation (README, ROADMAP, state.json)
- Update architecture blueprint and Cockatrice analysis
- All Phase 2 deliverables complete and documented
2026-07-25 02:56:29 +00:00

247 lines
7.7 KiB
Python

"""Deck manager service."""
from typing import List, Dict, Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete
from sqlalchemy.orm import selectinload
from app.models.user_deck import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard
from app.models.models import MtgonlineCard
class DeckManager:
"""Deck manager service."""
@staticmethod
async def create_deck(
db: AsyncSession,
user_id: int,
name: str,
folder_id: Optional[int] = None,
format: str = "standard",
notes: Optional[str] = None,
is_precedent: bool = False,
precedent_name: Optional[str] = None,
) -> UserDeck:
"""Create a new deck."""
deck = UserDeck(
user_id=user_id,
name=name,
folder_id=folder_id,
format=format,
notes=notes,
is_precedent=is_precedent,
precedent_name=precedent_name,
)
db.add(deck)
await db.flush()
return deck
@staticmethod
async def get_deck(db: AsyncSession, deck_id: int, user_id: int) -> Optional[UserDeck]:
"""Get a deck by ID."""
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def list_decks(
db: AsyncSession,
user_id: int,
status_filter: Optional[str] = None,
folder_id: Optional[int] = None,
is_precedent: Optional[bool] = None,
page: int = 1,
page_size: int = 50,
) -> List[UserDeck]:
"""List user's decks with filtering."""
conditions = [UserDeck.user_id == user_id]
if status_filter:
conditions.append(UserDeck.status == status_filter)
if folder_id:
conditions.append(UserDeck.folder_id == folder_id)
if is_precedent is not None:
conditions.append(UserDeck.is_precedent == is_precedent)
offset = (page - 1) * page_size
stmt = select(UserDeck).where(*conditions).order_by(UserDeck.updated_at.desc()).offset(offset).limit(page_size)
result = await db.execute(stmt)
return result.scalars().all()
@staticmethod
async def update_deck(
db: AsyncSession,
deck_id: int,
user_id: int,
name: Optional[str] = None,
folder_id: Optional[int] = None,
format: Optional[str] = None,
notes: Optional[str] = None,
) -> Optional[UserDeck]:
"""Update a deck."""
deck = await DeckManager.get_deck(db, deck_id, user_id)
if not deck:
return None
if deck.status == "FINAL":
raise ValueError("Cannot modify a finalized deck")
if name:
deck.name = name
if folder_id is not None:
deck.folder_id = folder_id
if format:
deck.format = format
if notes is not None:
deck.notes = notes
await db.flush()
return deck
@staticmethod
async def delete_deck(db: AsyncSession, deck_id: int, user_id: int) -> bool:
"""Delete a deck."""
deck = await DeckManager.get_deck(db, deck_id, user_id)
if not deck:
return False
await db.execute(delete(UserDeck).where(UserDeck.id == deck_id))
await db.flush()
return True
@staticmethod
async def finalize_deck(db: AsyncSession, deck_id: int, user_id: int) -> Optional[UserDeck]:
"""Transition a deck from DRAFT to FINAL status."""
deck = await DeckManager.get_deck(db, deck_id, user_id)
if not deck:
return None
if deck.status == "FINAL":
raise ValueError("Deck is already finalized")
# Check deck has cards
card_count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
card_count_result = await db.execute(card_count_stmt)
card_count = card_count_result.scalar() or 0
if card_count == 0:
raise ValueError("Cannot finalize an empty deck")
deck.status = "FINAL"
await db.flush()
return deck
@staticmethod
async def add_card_to_deck(
db: AsyncSession,
deck_id: int,
card_id: int,
quantity: int = 1,
zone: str = "main",
position: Optional[int] = None,
) -> UserDeckCard:
"""Add a card to a deck."""
deck_card = UserDeckCard(
deck_id=deck_id,
card_id=card_id,
quantity=quantity,
zone=zone,
position=position,
)
db.add(deck_card)
await db.flush()
return deck_card
@staticmethod
async def get_deck_cards(db: AsyncSession, deck_id: int, zone: Optional[str] = None) -> List[UserDeckCard]:
"""Get cards in a deck."""
conditions = [UserDeckCard.deck_id == deck_id]
if zone:
conditions.append(UserDeckCard.zone == zone)
stmt = select(UserDeckCard).where(*conditions).order_by(UserDeckCard.id)
result = await db.execute(stmt)
return result.scalars().all()
@staticmethod
async def update_deck_card(
db: AsyncSession,
deck_card_id: int,
quantity: Optional[int] = None,
zone: Optional[str] = None,
position: Optional[int] = None,
) -> Optional[UserDeckCard]:
"""Update a card in a deck."""
stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card_id)
result = await db.execute(stmt)
deck_card = result.scalar_one_or_none()
if not deck_card:
return None
if quantity is not None:
deck_card.quantity = quantity
if zone:
deck_card.zone = zone
if position is not None:
deck_card.position = position
await db.flush()
return deck_card
@staticmethod
async def remove_card_from_deck(db: AsyncSession, deck_card_id: int) -> bool:
"""Remove a card from a deck."""
stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card_id)
result = await db.execute(stmt)
deck_card = result.scalar_one_or_none()
if not deck_card:
return False
await db.execute(delete(UserDeckCard).where(UserDeckCard.id == deck_card_id))
await db.flush()
return True
@staticmethod
async def clone_precedent(
db: AsyncSession,
precedent_id: int,
user_id: int,
name: Optional[str] = None,
) -> UserDeck:
"""Clone a precedent into a new deck."""
# Get precedent
stmt = select(DeckPrecedent).where(DeckPrecedent.id == precedent_id)
result = await db.execute(stmt)
precedent = result.scalar_one_or_none()
if not precedent:
raise ValueError(f"Precedent {precedent_id} not found")
# Create new deck
new_name = name or f"Copy of {precedent.name}"
new_deck = UserDeck(
user_id=user_id,
name=new_name,
format=precedent.format,
is_precedent=False,
)
db.add(new_deck)
await db.flush()
# Copy cards from precedent
card_stmt = select(DeckPrecedentCard).where(DeckPrecedentCard.precedent_id == precedent_id)
card_result = await db.execute(card_stmt)
precedent_cards = card_result.scalars().all()
for pc in precedent_cards:
new_dc = UserDeckCard(
deck_id=new_deck.id,
card_id=pc.card_id,
quantity=pc.quantity,
zone=pc.zone,
)
db.add(new_dc)
await db.flush()
return new_deck