- Switched from psycopg2 to asyncpg for async SQLAlchemy support - Fixed router registration in main.py - removed duplicate prefixes - Added user_data export to routers/__init__.py - Refactored decks router to use DeckManager service layer - Integrated FuzzyCardMatcher into card_router search endpoints - Made WishlistCreate.card_id optional for proper schema validation - Set PostgreSQL password and configured scram-sha-256 auth - Updated alembic.ini to use local PostgreSQL instead of Docker hostname - Created generic_schemas.py for reusable schema patterns - Added test_routers.py and test_schema_validation.py test files - All 6 Alembic migrations applied successfully (37 tables created) - Application running on port 8000 with all services connected
722 lines
25 KiB
Python
722 lines
25 KiB
Python
"""
|
|
Deck management router endpoints for per-user deck building.
|
|
|
|
Provides CRUD operations for user decks with card management,
|
|
deck finalization, precedent templates, and card search integration.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, or_, update, delete
|
|
from typing import Optional, List
|
|
|
|
from app.core.database import get_db, mtg_get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.models import User, DecklistFolder, MtgonlineCard
|
|
from app.models.mtg_models import MtgCard, MtgSet
|
|
from app.models.user_deck import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
|
|
from app.schemas.user_deck_schemas import (
|
|
UserDeckCreate, UserDeckUpdate, UserDeckResponse, UserDeckListResponse,
|
|
DeckCardCreate, DeckCardUpdate, DeckCardResponse, DeckCardWithDetailsResponse, DeckCardListResponse,
|
|
PrecedentCreate, PrecedentUpdate, PrecedentResponse, PrecedentListResponse,
|
|
SuggestionCreate, SuggestionResponse, SuggestionListResponse,
|
|
DeckFinalizeRequest, DeckFinalizeResponse,
|
|
CardSearchRequest,
|
|
)
|
|
from app.schemas.card_search_schemas import CardSearchResponse
|
|
from app.schemas.generic_schemas import MessageResponse, CountResponse
|
|
from app.services.deck_manager import DeckManager
|
|
from app.services.deck_parser import DeckParser
|
|
|
|
router = APIRouter()
|
|
|
|
_deck_mgr = DeckManager()
|
|
_deck_parser = DeckParser()
|
|
|
|
|
|
# ===== Deck CRUD =====
|
|
|
|
@router.get("/", response_model=UserDeckListResponse)
|
|
async def list_user_decks(
|
|
status_filter: Optional[str] = None,
|
|
folder_id: Optional[int] = None,
|
|
is_precedent: Optional[bool] = None,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""List user's decks with optional filtering."""
|
|
user_id = int(current_user["user_id"])
|
|
try:
|
|
decks = await _deck_mgr.list_decks(
|
|
db=db,
|
|
user_id=user_id,
|
|
status_filter=status_filter,
|
|
folder_id=folder_id,
|
|
is_precedent=is_precedent,
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|
|
|
|
# Build response with card counts
|
|
deck_ids = [d.id for d in decks]
|
|
card_counts = {}
|
|
if deck_ids:
|
|
count_subquery = (
|
|
select(UserDeckCard.deck_id, func.count().label('cnt'))
|
|
.where(UserDeckCard.deck_id.in_(deck_ids))
|
|
.group_by(UserDeckCard.deck_id)
|
|
.subquery()
|
|
)
|
|
count_stmt = select(count_subquery.c.deck_id, count_subquery.c.cnt).where(
|
|
count_subquery.c.deck_id.in_(deck_ids)
|
|
)
|
|
count_result = await db.execute(count_stmt)
|
|
card_counts = {row[0]: row[1] for row in count_result.fetchall()}
|
|
|
|
deck_responses = []
|
|
for deck in decks:
|
|
deck_data = UserDeckResponse.model_validate(deck)
|
|
deck_data.card_count = card_counts.get(deck.id, 0)
|
|
deck_data.is_owner = True
|
|
deck_responses.append(deck_data)
|
|
|
|
total = len(decks)
|
|
return UserDeckListResponse(
|
|
decks=deck_responses,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size,
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
|
|
@router.post("/", response_model=UserDeckResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_user_deck(
|
|
request: UserDeckCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new user deck (DRAFT status)."""
|
|
user_id = int(current_user["user_id"])
|
|
try:
|
|
new_deck = await _deck_mgr.create_deck(
|
|
db=db,
|
|
user_id=user_id,
|
|
name=request.name,
|
|
folder_id=request.folder_id,
|
|
format=request.format,
|
|
notes=request.notes,
|
|
is_precedent=request.is_precedent,
|
|
precedent_name=request.precedent_name,
|
|
)
|
|
deck_data = UserDeckResponse.model_validate(new_deck)
|
|
deck_data.card_count = 0
|
|
deck_data.is_owner = True
|
|
return deck_data
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
|
|
@router.get("/{deck_id}", response_model=UserDeckResponse)
|
|
async def get_user_deck(
|
|
deck_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a specific user deck."""
|
|
user_id = int(current_user["user_id"])
|
|
try:
|
|
deck = await _deck_mgr.get_deck(db=db, deck_id=deck_id, user_id=user_id)
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
# Get card count
|
|
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
|
count_result = await db.execute(count_stmt)
|
|
card_count = count_result.scalar() or 0
|
|
|
|
deck_data = UserDeckResponse.model_validate(deck)
|
|
deck_data.card_count = card_count
|
|
deck_data.is_owner = True
|
|
return deck_data
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
|
|
@router.patch("/{deck_id}", response_model=UserDeckResponse)
|
|
async def update_user_deck(
|
|
deck_id: int,
|
|
request: UserDeckUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a user deck."""
|
|
user_id = int(current_user["user_id"])
|
|
try:
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
if "status" in update_data and update_data["status"]:
|
|
update_data["status"] = update_data["status"].value
|
|
|
|
updated_deck = await _deck_mgr.update_deck(
|
|
db=db,
|
|
deck_id=deck_id,
|
|
user_id=user_id,
|
|
**update_data,
|
|
)
|
|
|
|
if not updated_deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
# Get card count
|
|
count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
|
count_result = await db.execute(count_stmt)
|
|
card_count = count_result.scalar() or 0
|
|
|
|
deck_data = UserDeckResponse.model_validate(updated_deck)
|
|
deck_data.card_count = card_count
|
|
deck_data.is_owner = True
|
|
return deck_data
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
|
|
@router.delete("/{deck_id}", response_model=MessageResponse)
|
|
async def delete_user_deck(
|
|
deck_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a user deck."""
|
|
user_id = int(current_user["user_id"])
|
|
try:
|
|
result = await _deck_mgr.delete_deck(db=db, deck_id=deck_id, user_id=user_id)
|
|
if not result:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
return MessageResponse(message="Deck deleted successfully")
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
|
|
# ===== Deck Finalize =====
|
|
|
|
@router.post("/{deck_id}/finalize", response_model=DeckFinalizeResponse)
|
|
async def finalize_user_deck(
|
|
deck_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Transition a deck from DRAFT to FINAL status."""
|
|
user_id = int(current_user["user_id"])
|
|
try:
|
|
deck = await _deck_mgr.finalize_deck(db=db, deck_id=deck_id, user_id=user_id)
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
return DeckFinalizeResponse(
|
|
deck_id=deck_id,
|
|
status="FINAL",
|
|
message="Deck finalized successfully",
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
|
|
# ===== Deck Card Management =====
|
|
|
|
@router.post("/{deck_id}/cards", response_model=DeckCardResponse, status_code=status.HTTP_201_CREATED)
|
|
async def add_deck_card(
|
|
deck_id: int,
|
|
request: DeckCardCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a card to a user's deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify deck exists and belongs to user
|
|
deck_stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
|
deck_result = await db.execute(deck_stmt)
|
|
deck = deck_result.scalar_one_or_none()
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
if deck.status == "FINAL":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
|
|
|
|
# Verify card exists in local mirror
|
|
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id == request.card_id)
|
|
card_result = await db.execute(card_stmt)
|
|
card = card_result.scalar_one_or_none()
|
|
if not card:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in database")
|
|
|
|
# Check for duplicate (same card, same zone)
|
|
existing_stmt = select(UserDeckCard).where(
|
|
UserDeckCard.deck_id == deck_id,
|
|
UserDeckCard.card_id == request.card_id,
|
|
UserDeckCard.zone == request.zone.value,
|
|
)
|
|
existing_result = await db.execute(existing_stmt)
|
|
existing = existing_result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
# Update quantity
|
|
new_qty = existing.quantity + request.quantity
|
|
stmt = update(UserDeckCard).where(UserDeckCard.id == existing.id).values(quantity=new_qty)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserDeckCard).where(UserDeckCard.id == existing.id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
else:
|
|
new_card = UserDeckCard(
|
|
deck_id=deck_id,
|
|
card_id=request.card_id,
|
|
quantity=request.quantity,
|
|
zone=request.zone.value,
|
|
position=request.position,
|
|
)
|
|
db.add(new_card)
|
|
await db.flush()
|
|
updated = new_card
|
|
|
|
return DeckCardResponse.model_validate(updated)
|
|
|
|
|
|
@router.get("/{deck_id}/cards", response_model=DeckCardListResponse)
|
|
async def get_deck_cards(
|
|
deck_id: int,
|
|
zone: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get all cards in a user's deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify deck exists and belongs to user
|
|
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
# Use service to get cards
|
|
deck_cards = await _deck_mgr.get_deck_cards(db=db, deck_id=deck_id, zone=zone)
|
|
|
|
# Fetch card details from local mirror
|
|
card_ids = [dc.card_id for dc in deck_cards]
|
|
card_details = {}
|
|
if card_ids:
|
|
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids))
|
|
card_result = await db.execute(card_stmt)
|
|
for c in card_result.scalars().all():
|
|
card_details[c.id] = c
|
|
|
|
card_responses = []
|
|
for dc in deck_cards:
|
|
card = card_details.get(dc.card_id)
|
|
response = DeckCardWithDetailsResponse.model_validate(dc)
|
|
response.card_name = card.name if card else f"Card#{dc.card_id}"
|
|
response.card_type_line = card.type_line if card else ""
|
|
response.card_image = card.image if card else None
|
|
card_responses.append(response)
|
|
|
|
return DeckCardListResponse(cards=card_responses, total=len(card_responses))
|
|
|
|
|
|
@router.patch("/{deck_id}/cards/{card_id}", response_model=DeckCardResponse)
|
|
async def update_deck_card(
|
|
deck_id: int,
|
|
card_id: int,
|
|
request: DeckCardUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a card's quantity or zone in a deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify deck exists and belongs to user
|
|
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
if deck.status == "FINAL":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
|
|
|
|
# Find the card entry
|
|
stmt = select(UserDeckCard).where(
|
|
UserDeckCard.deck_id == deck_id,
|
|
UserDeckCard.card_id == card_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
deck_card = result.scalar_one_or_none()
|
|
|
|
if not deck_card:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in deck")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
if "zone" in update_data and update_data["zone"]:
|
|
update_data["zone"] = update_data["zone"].value
|
|
|
|
stmt = update(UserDeckCard).where(UserDeckCard.id == deck_card.id).values(**update_data)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card.id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
|
|
return DeckCardResponse.model_validate(updated)
|
|
|
|
|
|
@router.delete("/{deck_id}/cards/{card_id}", response_model=MessageResponse)
|
|
async def remove_deck_card(
|
|
deck_id: int,
|
|
card_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Remove a card from a user's deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify deck exists and belongs to user
|
|
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
if deck.status == "FINAL":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
|
|
|
|
# Find the card entry
|
|
stmt = select(UserDeckCard).where(
|
|
UserDeckCard.deck_id == deck_id,
|
|
UserDeckCard.card_id == card_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
deck_card = result.scalar_one_or_none()
|
|
|
|
if not deck_card:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in deck")
|
|
|
|
await db.execute(delete(UserDeckCard).where(UserDeckCard.id == deck_card.id))
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Card removed from deck")
|
|
|
|
|
|
# ===== Deck Precedents =====
|
|
# Note: Precedent endpoints use direct DB operations as DeckManager
|
|
# does not yet have precedent-specific methods.
|
|
|
|
@router.get("/precedents", response_model=PrecedentListResponse)
|
|
async def list_precedents(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
format_filter: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""List available deck precedents."""
|
|
offset = (page - 1) * page_size
|
|
|
|
conditions = [DeckPrecedent.is_public == True]
|
|
if format_filter:
|
|
conditions.append(DeckPrecedent.format == format_filter)
|
|
|
|
# Count total
|
|
count_stmt = select(func.count()).select_from(DeckPrecedent).where(*conditions)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar() or 0
|
|
|
|
# Fetch precedents
|
|
stmt = select(DeckPrecedent).where(*conditions).order_by(DeckPrecedent.created_at.desc()).offset(offset).limit(page_size)
|
|
result = await db.execute(stmt)
|
|
precedents = result.scalars().all()
|
|
|
|
# Get card counts
|
|
prec_ids = [p.id for p in precedents]
|
|
card_counts = {}
|
|
if prec_ids:
|
|
count_subquery = (
|
|
select(DeckPrecedentCard.precedent_id, func.count().label('cnt'))
|
|
.where(DeckPrecedentCard.precedent_id.in_(prec_ids))
|
|
.group_by(DeckPrecedentCard.precedent_id)
|
|
.subquery()
|
|
)
|
|
count_stmt = select(count_subquery.c.precedent_id, count_subquery.c.cnt).where(
|
|
count_subquery.c.precedent_id.in_(prec_ids)
|
|
)
|
|
count_result = await db.execute(count_stmt)
|
|
card_counts = {row[0]: row[1] for row in count_result.fetchall()}
|
|
|
|
prec_responses = []
|
|
for p in precedents:
|
|
prec_data = PrecedentResponse.model_validate(p)
|
|
prec_data.card_count = card_counts.get(p.id, 0)
|
|
prec_responses.append(prec_data)
|
|
|
|
return PrecedentListResponse(
|
|
precedents=prec_responses,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size,
|
|
)
|
|
|
|
|
|
@router.post("/precedents", response_model=PrecedentResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_precedent(
|
|
request: PrecedentCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a deck precedent (template)."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
new_precedent = DeckPrecedent(
|
|
name=request.name,
|
|
description=request.description,
|
|
format=request.format,
|
|
is_public=request.is_public,
|
|
created_by=user_id,
|
|
)
|
|
db.add(new_precedent)
|
|
await db.flush()
|
|
|
|
prec_data = PrecedentResponse.model_validate(new_precedent)
|
|
prec_data.card_count = 0
|
|
return prec_data
|
|
|
|
|
|
@router.get("/precedents/{precedent_id}", response_model=PrecedentResponse)
|
|
async def get_precedent(
|
|
precedent_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a specific deck precedent."""
|
|
stmt = select(DeckPrecedent).where(DeckPrecedent.id == precedent_id)
|
|
result = await db.execute(stmt)
|
|
precedent = result.scalar_one_or_none()
|
|
|
|
if not precedent:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Precedent not found")
|
|
|
|
# Get card count
|
|
count_stmt = select(func.count()).select_from(DeckPrecedentCard).where(DeckPrecedentCard.precedent_id == precedent_id)
|
|
count_result = await db.execute(count_stmt)
|
|
card_count = count_result.scalar() or 0
|
|
|
|
prec_data = PrecedentResponse.model_validate(precedent)
|
|
prec_data.card_count = card_count
|
|
return prec_data
|
|
|
|
|
|
@router.post("/precedents/{precedent_id}/use")
|
|
async def use_precedent(
|
|
precedent_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Clone a precedent into a new draft deck for the current user."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
try:
|
|
new_deck = await _deck_mgr.clone_precedent(
|
|
db=db,
|
|
precedent_id=precedent_id,
|
|
user_id=user_id,
|
|
)
|
|
return {
|
|
"message": "Precedent cloned into new deck",
|
|
"deck_id": new_deck.id,
|
|
"deck_name": new_deck.name,
|
|
"card_count": len(await _deck_mgr.get_deck_cards(db=db, deck_id=new_deck.id)),
|
|
}
|
|
except ValueError as e:
|
|
if "not found" in str(e).lower():
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
|
|
# ===== Card Search Integration =====
|
|
|
|
@router.post("/search/cards", response_model=CardSearchResponse)
|
|
async def search_cards_for_deck(
|
|
request: CardSearchRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Search MTG cards for use in deck building."""
|
|
offset = (request.offset // request.limit) * request.limit
|
|
|
|
# Search across multiple fields using local mirror
|
|
stmt = (
|
|
select(MtgonlineCard)
|
|
.where(
|
|
or_(
|
|
MtgonlineCard.name.ilike(f"%{request.query}%"),
|
|
MtgonlineCard.type_line.ilike(f"%{request.query}%"),
|
|
MtgonlineCard.mana_cost.ilike(f"%{request.query}%"),
|
|
)
|
|
)
|
|
.offset(offset)
|
|
.limit(request.limit)
|
|
)
|
|
|
|
result = await db.execute(stmt)
|
|
cards = result.scalars().all()
|
|
|
|
card_list = []
|
|
for card in cards:
|
|
card_data = {
|
|
"id": card.id,
|
|
"name": card.name,
|
|
"mana_cost": card.mana_cost,
|
|
"type_line": card.type_line,
|
|
"oracle_text": card.oracle_text,
|
|
"power": card.power,
|
|
"toughness": card.toughness,
|
|
"rarity": card.rarity,
|
|
"layout": card.layout,
|
|
"artist": card.artist,
|
|
"flavor_text": card.flavor_text,
|
|
"set_code": card.set_code,
|
|
"set_name": card.set_name,
|
|
"identifiers": card.identifiers,
|
|
"images": card.images,
|
|
}
|
|
card_list.append(card_data)
|
|
|
|
# Get total count
|
|
count_stmt = select(func.count()).select_from(MtgonlineCard).where(
|
|
or_(
|
|
MtgonlineCard.name.ilike(f"%{request.query}%"),
|
|
MtgonlineCard.type_line.ilike(f"%{request.query}%"),
|
|
MtgonlineCard.mana_cost.ilike(f"%{request.query}%"),
|
|
)
|
|
)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar() or 0
|
|
|
|
return CardSearchResponse(
|
|
cards=card_list,
|
|
total=total,
|
|
page=request.offset // request.limit + 1,
|
|
page_size=request.limit,
|
|
total_pages=(total + request.limit - 1) // request.limit,
|
|
)
|
|
|
|
|
|
# ===== Card Suggestions =====
|
|
# Note: Suggestion endpoints use direct DB operations as DeckManager
|
|
# does not yet have suggestion-specific methods.
|
|
|
|
@router.get("/{deck_id}/suggestions", response_model=SuggestionListResponse)
|
|
async def get_deck_suggestions(
|
|
deck_id: int,
|
|
suggestion_type: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get card suggestions for a deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify deck exists and belongs to user
|
|
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
conditions = [CardSuggestion.deck_id == deck_id]
|
|
if suggestion_type:
|
|
conditions.append(CardSuggestion.suggestion_type == suggestion_type)
|
|
|
|
# Fetch suggestions
|
|
stmt = select(CardSuggestion).where(*conditions).order_by(CardSuggestion.created_at.desc())
|
|
result = await db.execute(stmt)
|
|
suggestions = result.scalars().all()
|
|
|
|
# Fetch card names from local mirror
|
|
card_ids = [s.card_id for s in suggestions]
|
|
card_names = {}
|
|
if card_ids:
|
|
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids))
|
|
card_result = await db.execute(card_stmt)
|
|
for c in card_result.scalars().all():
|
|
card_names[c.id] = c.name
|
|
|
|
sugg_responses = []
|
|
for s in suggestions:
|
|
sugg_data = SuggestionResponse.model_validate(s)
|
|
sugg_data.card_name = card_names.get(s.card_id, f"Card#{s.card_id}")
|
|
sugg_responses.append(sugg_data)
|
|
|
|
return SuggestionListResponse(
|
|
suggestions=sugg_responses,
|
|
total=len(sugg_responses),
|
|
)
|
|
|
|
|
|
@router.post("/{deck_id}/suggestions", response_model=SuggestionResponse, status_code=status.HTTP_201_CREATED)
|
|
async def add_suggestion(
|
|
deck_id: int,
|
|
request: SuggestionCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a card suggestion to a deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify deck exists and belongs to user
|
|
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found")
|
|
|
|
if deck.status == "FINAL":
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot modify a finalized deck")
|
|
|
|
new_suggestion = CardSuggestion(
|
|
deck_id=deck_id,
|
|
card_id=request.card_id,
|
|
source_card_id=request.source_card_id,
|
|
suggestion_type=request.suggestion_type.value,
|
|
confidence=request.confidence,
|
|
notes=request.notes,
|
|
)
|
|
db.add(new_suggestion)
|
|
await db.flush()
|
|
|
|
return SuggestionResponse.model_validate(new_suggestion)
|