""" Router: featured cards Provides endpoints that surface curated "featured" card selections for use in automated content pipelines (n8n, Discord scheduled posts, etc.). Endpoints --------- GET /api/v2/cards/featured/card-of-the-week Returns the single highest-rated eligible card created in the past N days, with all fields needed to build a Discord embed in one call. Deterministic for a given calendar day (cache-friendly). Design notes ------------ - "Card created" is approximated by Pack.open_time — the timestamp when the pack containing the card was opened. The Card model has no created_at column; open_time is the closest available proxy and is semantically correct (the card enters a team's collection at pack-open time). - Rating is Card.value (the integer rating field set at card issuance). - Tiebreak: highest value desc, then newest pack open_time desc, then highest card.id desc — fully deterministic with no random element. - AI teams (Team.is_ai is not NULL and != 0) are excluded by default. Pass ?include_ai=true to lift that filter. """ from datetime import datetime, timedelta from typing import Optional import pydantic from fastapi import APIRouter, HTTPException, Query from ..db_engine import Card, Cardset, Pack, Player, Rarity, Team router = APIRouter(prefix="/api/v2/cards/featured", tags=["featured"]) class CardOfTheWeekResponse(pydantic.BaseModel): """Single-call Discord embed payload for the card-of-the-week feature. All fields needed for a rich embed are present so the consumer (n8n, Discord bot) never needs a follow-up API call. """ card_id: int player_name: str team_name: str team_abbrev: str rarity_name: str rating: int card_image_url: str cardset_name: str card_created_at: Optional[str] = ( None # ISO-8601 string; None when pack has no open_time ) @router.get( "/card-of-the-week", response_model=CardOfTheWeekResponse, summary="Card of the Week", description=( "Returns the highest-rated card created (pack opened) within the last N days. " "Excludes AI-team cards by default. Deterministic within a calendar day." ), ) async def card_of_the_week( days: int = Query(default=7, ge=1, le=90, description="Look-back window in days"), include_ai: bool = Query( default=False, description="Include cards owned by AI teams (excluded by default)", ), ): """Return the card-of-the-week for use in Discord embeds and n8n automation. Selection algorithm: 1. Filter cards whose pack was opened within the last `days` days. 2. Exclude AI-team cards unless include_ai=true. 3. Order by Card.value DESC, Pack.open_time DESC, Card.id DESC. 4. Return the first row as a flat Discord-ready payload. Returns 404 when no eligible card exists in the window. """ cutoff = datetime.utcnow() - timedelta(days=days) query = ( Card.select(Card, Pack, Player, Team, Rarity, Cardset) .join(Pack, on=(Card.pack == Pack.id)) .switch(Card) .join(Player, on=(Card.player == Player.player_id)) .join(Rarity, on=(Player.rarity == Rarity.id)) .switch(Player) .join(Cardset, on=(Player.cardset == Cardset.id)) .switch(Card) .join(Team, on=(Card.team == Team.id)) .where(Pack.open_time >= cutoff) .where(Card.player.is_null(False)) .where(Card.team.is_null(False)) .where(Card.pack.is_null(False)) ) if not include_ai: # is_ai is an IntegerField(null=True); non-AI teams have is_ai=0 or NULL query = query.where((Team.is_ai == 0) | (Team.is_ai.is_null(True))) # Deterministic tiebreak: highest rating first, then newest pack, then card id query = query.order_by(Card.value.desc(), Pack.open_time.desc(), Card.id.desc()) card = query.first() if card is None: raise HTTPException(status_code=404, detail="no featured card this week") pack_open_time = None if card.pack and card.pack.open_time: ot = card.pack.open_time pack_open_time = ot.isoformat() if hasattr(ot, "isoformat") else str(ot) return CardOfTheWeekResponse( card_id=card.id, player_name=card.player.p_name, team_name=card.team.sname, team_abbrev=card.team.abbrev, rarity_name=card.player.rarity.name, rating=card.value, card_image_url=card.player.image, cardset_name=card.player.cardset.name, card_created_at=pack_open_time, )