- 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
247 lines
7.7 KiB
Python
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
|