paper-dynasty-database/app/routers_v2/season_stats.py
Cal Corum a2d2aa3d31 feat(WP-13): post-game callback endpoints for season stats and evolution
Implements two new API endpoints the bot calls after a game completes:

  POST /api/v2/season-stats/update-game/{game_id}
    Delegates to update_season_stats() service (WP-05). Returns
    {"updated": N, "skipped": bool} with idempotency via ProcessedGame ledger.

  POST /api/v2/evolution/evaluate-game/{game_id}
    Finds all (player_id, team_id) pairs from the game's StratPlay rows,
    calls evaluate_card() for each pair that has an EvolutionCardState,
    and returns {"evaluated": N, "tier_ups": [...]} with full tier-up detail.

New files:
  app/services/evolution_evaluator.py — evaluate_card() service (WP-08)
  tests/test_postgame_evolution.py    — 10 integration tests (all pass)

Modified files:
  app/routers_v2/season_stats.py — rewritten to delegate to the service
  app/routers_v2/evolution.py    — evaluate-game endpoint added
  app/main.py                    — season_stats router registered

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 16:05:21 -05:00

62 lines
2.2 KiB
Python

"""Season stats API endpoints.
Covers WP-13 (Post-Game Callback Integration):
POST /api/v2/season-stats/update-game/{game_id}
Delegates to app.services.season_stats.update_season_stats() which
aggregates StratPlay and Decision rows for a completed game and
performs an additive upsert into player_season_stats.
Idempotency is enforced by the service layer: re-delivery of the same
game_id returns {"updated": 0, "skipped": true} without modifying stats.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import oauth2_scheme, valid_token
router = APIRouter(prefix="/api/v2/season-stats", tags=["season-stats"])
logger = logging.getLogger(__name__)
@router.post("/update-game/{game_id}")
async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_scheme)):
"""Increment season stats with batting and pitching deltas from a game.
Calls update_season_stats(game_id) from the service layer which:
- Aggregates all StratPlay rows by (player_id, team_id, season)
- Merges Decision rows into pitching groups
- Performs an additive ON CONFLICT upsert into player_season_stats
- Guards against double-counting via the last_game FK check
Response: {"updated": N, "skipped": false}
- N: total player_season_stats rows upserted (batters + pitchers)
- skipped: true when this game_id was already processed (idempotent re-delivery)
Errors from the service are logged but re-raised as 500 so the bot
knows to retry.
"""
if not valid_token(token):
logging.warning("Bad Token: [REDACTED]")
raise HTTPException(status_code=401, detail="Unauthorized")
from ..services.season_stats import update_season_stats
try:
result = update_season_stats(game_id)
except Exception as exc:
logger.error("update-game/%d failed: %s", game_id, exc, exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Season stats update failed for game {game_id}: {exc}",
)
updated = result.get("batters_updated", 0) + result.get("pitchers_updated", 0)
return {
"updated": updated,
"skipped": result.get("skipped", False),
}