"""Season stats API endpoints. Covers WP-13 (Post-Game Callback Integration): POST /api/v2/season-stats/update-game/{game_id} Aggregates BattingStat and PitchingStat rows for a completed game and increments the corresponding batting_season_stats / pitching_season_stats rows via an additive upsert. """ import logging from fastapi import APIRouter, Depends, HTTPException from ..db_engine import db from ..dependencies import oauth2_scheme, valid_token router = APIRouter(prefix="/api/v2/season-stats", tags=["season-stats"]) def _ip_to_outs(ip: float) -> int: """Convert innings-pitched float (e.g. 6.1) to integer outs (e.g. 19). Baseball stores IP as whole.partial where the fractional digit is outs (0, 1, or 2), not tenths. 6.1 = 6 innings + 1 out = 19 outs. """ whole = int(ip) partial = round((ip - whole) * 10) return whole * 3 + partial @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. Queries BattingStat and PitchingStat rows for game_id, aggregates by (player_id, team_id, season), then performs an additive ON CONFLICT upsert into batting_season_stats and pitching_season_stats respectively. Replaying the same game_id will double-count stats, so callers must ensure this is only called once per game. Response: {"updated": N} where N is the number of player rows touched. """ if not valid_token(token): logging.warning("Bad Token: [REDACTED]") raise HTTPException(status_code=401, detail="Unauthorized") updated = 0 # --- Batting --- bat_rows = list( db.execute_sql( """ SELECT c.player_id, bs.team_id, bs.season, SUM(bs.pa), SUM(bs.ab), SUM(bs.run), SUM(bs.hit), SUM(bs.double), SUM(bs.triple), SUM(bs.hr), SUM(bs.rbi), SUM(bs.bb), SUM(bs.so), SUM(bs.hbp), SUM(bs.sac), SUM(bs.ibb), SUM(bs.gidp), SUM(bs.sb), SUM(bs.cs) FROM battingstat bs JOIN card c ON bs.card_id = c.id WHERE bs.game_id = %s GROUP BY c.player_id, bs.team_id, bs.season """, (game_id,), ) ) for row in bat_rows: ( player_id, team_id, season, pa, ab, runs, hits, doubles, triples, hr, rbi, bb, strikeouts, hbp, sac, ibb, gidp, sb, cs, ) = row db.execute_sql( """ INSERT INTO batting_season_stats (player_id, team_id, season, pa, ab, runs, hits, doubles, triples, hr, rbi, bb, strikeouts, hbp, sac, ibb, gidp, sb, cs) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (player_id, team_id, season) DO UPDATE SET pa = batting_season_stats.pa + EXCLUDED.pa, ab = batting_season_stats.ab + EXCLUDED.ab, runs = batting_season_stats.runs + EXCLUDED.runs, hits = batting_season_stats.hits + EXCLUDED.hits, doubles = batting_season_stats.doubles + EXCLUDED.doubles, triples = batting_season_stats.triples + EXCLUDED.triples, hr = batting_season_stats.hr + EXCLUDED.hr, rbi = batting_season_stats.rbi + EXCLUDED.rbi, bb = batting_season_stats.bb + EXCLUDED.bb, strikeouts= batting_season_stats.strikeouts+ EXCLUDED.strikeouts, hbp = batting_season_stats.hbp + EXCLUDED.hbp, sac = batting_season_stats.sac + EXCLUDED.sac, ibb = batting_season_stats.ibb + EXCLUDED.ibb, gidp = batting_season_stats.gidp + EXCLUDED.gidp, sb = batting_season_stats.sb + EXCLUDED.sb, cs = batting_season_stats.cs + EXCLUDED.cs """, ( player_id, team_id, season, pa, ab, runs, hits, doubles, triples, hr, rbi, bb, strikeouts, hbp, sac, ibb, gidp, sb, cs, ), ) updated += 1 # --- Pitching --- pit_rows = list( db.execute_sql( """ SELECT c.player_id, ps.team_id, ps.season, SUM(ps.ip), SUM(ps.so), SUM(ps.hit), SUM(ps.run), SUM(ps.erun), SUM(ps.bb), SUM(ps.hbp), SUM(ps.wp), SUM(ps.balk), SUM(ps.hr), SUM(ps.gs), SUM(ps.win), SUM(ps.loss), SUM(ps.hold), SUM(ps.sv), SUM(ps.bsv) FROM pitchingstat ps JOIN card c ON ps.card_id = c.id WHERE ps.game_id = %s GROUP BY c.player_id, ps.team_id, ps.season """, (game_id,), ) ) for row in pit_rows: ( player_id, team_id, season, ip, strikeouts, hits_allowed, runs_allowed, earned_runs, bb, hbp, wild_pitches, balks, hr_allowed, games_started, wins, losses, holds, saves, blown_saves, ) = row outs = _ip_to_outs(float(ip)) db.execute_sql( """ INSERT INTO pitching_season_stats (player_id, team_id, season, outs, strikeouts, hits_allowed, runs_allowed, earned_runs, bb, hbp, wild_pitches, balks, hr_allowed, games_started, wins, losses, holds, saves, blown_saves) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (player_id, team_id, season) DO UPDATE SET outs = pitching_season_stats.outs + EXCLUDED.outs, strikeouts = pitching_season_stats.strikeouts + EXCLUDED.strikeouts, hits_allowed= pitching_season_stats.hits_allowed+ EXCLUDED.hits_allowed, runs_allowed= pitching_season_stats.runs_allowed+ EXCLUDED.runs_allowed, earned_runs = pitching_season_stats.earned_runs + EXCLUDED.earned_runs, bb = pitching_season_stats.bb + EXCLUDED.bb, hbp = pitching_season_stats.hbp + EXCLUDED.hbp, wild_pitches= pitching_season_stats.wild_pitches+ EXCLUDED.wild_pitches, balks = pitching_season_stats.balks + EXCLUDED.balks, hr_allowed = pitching_season_stats.hr_allowed + EXCLUDED.hr_allowed, games_started= pitching_season_stats.games_started+ EXCLUDED.games_started, wins = pitching_season_stats.wins + EXCLUDED.wins, losses = pitching_season_stats.losses + EXCLUDED.losses, holds = pitching_season_stats.holds + EXCLUDED.holds, saves = pitching_season_stats.saves + EXCLUDED.saves, blown_saves = pitching_season_stats.blown_saves + EXCLUDED.blown_saves """, ( player_id, team_id, season, outs, strikeouts, hits_allowed, runs_allowed, earned_runs, bb, hbp, wild_pitches, balks, hr_allowed, games_started, wins, losses, holds, saves, blown_saves, ), ) updated += 1 logging.info(f"update-game/{game_id}: updated {updated} season stats rows") return {"updated": updated}