major-domo-v2/services/league_service.py
Cal Corum 37bf797254 Fix critical week rollover bugs causing 60x freeze message spam
Three bugs identified and fixed:

1. Deduplication logic tracked wrong week (transaction_freeze.py:216-219)
   - Saved freeze_from_week BEFORE _begin_freeze() modifies current.week
   - Prevents re-execution when API returns stale data

2. _run_transactions() bypassed service layer (transaction_freeze.py:350-394)
   - Added get_regular_transactions_by_week() to transaction_service.py
   - Now properly filters frozen=false and cancelled=false
   - Uses Transaction model objects instead of raw dict access

3. CRITICAL: Hardcoded current_id=1 (league_service.py:88-106)
   - Current table has one row PER SEASON, not a single row
   - Was patching Season 3 (id=1) instead of Season 13 (id=11)
   - Now fetches actual current state ID before patching

Root cause: The hardcoded ID caused every PATCH to update the wrong
season's record, so freeze was never actually set to True on the
current season. This caused the dedup check to pass 60 times (once
per minute during hour 0).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 14:15:26 -06:00

215 lines
7.7 KiB
Python

"""
League service for Discord Bot v2.0
Handles league-wide operations including current state, standings, and season information.
"""
import logging
from typing import Optional, List, Dict, Any
from config import get_config
from services.base_service import BaseService
from models.current import Current
from exceptions import APIException
logger = logging.getLogger(f'{__name__}.LeagueService')
class LeagueService(BaseService[Current]):
"""
Service for league-wide operations.
Features:
- Current league state retrieval
- Season standings
- League-wide statistics
"""
def __init__(self):
"""Initialize league service."""
super().__init__(Current, 'current')
logger.debug("LeagueService initialized")
async def get_current_state(self) -> Optional[Current]:
"""
Get the current league state including week, season, and settings.
Returns:
Current league state or None if not available
"""
try:
client = await self.get_client()
data = await client.get('current')
if data:
current = Current.from_api_data(data)
logger.debug(f"Retrieved current state: Week {current.week}, Season {current.season}")
return current
logger.debug("No current state data found")
return None
except Exception as e:
logger.error(f"Failed to get current league state: {e}")
return None
async def update_current_state(
self,
week: Optional[int] = None,
freeze: Optional[bool] = None
) -> Optional[Current]:
"""
Update current league state (week and/or freeze status).
This is typically used by automated tasks to increment the week
and toggle freeze status during weekly operations.
Args:
week: New week number (None to leave unchanged)
freeze: New freeze status (None to leave unchanged)
Returns:
Updated Current object or None if update failed
Raises:
APIException: If the update operation fails
"""
try:
# Build update data
update_data = {}
if week is not None:
update_data['week'] = week
if freeze is not None:
update_data['freeze'] = freeze
if not update_data:
logger.warning("update_current_state called with no updates")
return await self.get_current_state()
# Get the current state to find its actual ID
# (Current table has one row per season, NOT a single row with id=1)
current = await self.get_current_state()
if not current:
logger.error("Cannot update current state - unable to fetch current state")
return None
current_id = current.id
logger.debug(f"Updating current state id={current_id} (season {current.season})")
# Use BaseService patch method
updated_current = await self.patch(current_id, update_data)
if updated_current:
logger.info(f"Updated current state id={current_id}: {update_data}")
return updated_current
else:
logger.error(f"Failed to update current state id={current_id} - patch returned None")
return None
except Exception as e:
logger.error(f"Error updating current state: {e}")
raise APIException(f"Failed to update current state: {e}")
async def get_standings(self, season: Optional[int] = None) -> Optional[List[Dict[str, Any]]]:
"""
Get league standings for a season.
Args:
season: Season number (defaults to current season)
Returns:
List of standings data or None if not available
"""
try:
season = season or get_config().sba_season
client = await self.get_client()
data = await client.get('standings', params=[('season', str(season))])
if data and isinstance(data, list):
logger.debug(f"Retrieved standings for season {season}: {len(data)} teams")
return data
elif data and isinstance(data, dict):
# Handle case where API returns a dict with standings array
standings_data = data.get('standings', data.get('items', []))
if standings_data:
logger.debug(f"Retrieved standings for season {season}: {len(standings_data)} teams")
return standings_data
logger.debug(f"No standings data found for season {season}")
return None
except Exception as e:
logger.error(f"Failed to get standings for season {season}: {e}")
return None
async def get_division_standings(self, division_id: int, season: Optional[int] = None) -> Optional[List[Dict[str, Any]]]:
"""
Get standings for a specific division.
Args:
division_id: Division identifier
season: Season number (defaults to current season)
Returns:
List of division standings or None if not available
"""
try:
season = season or get_config().sba_season
client = await self.get_client()
data = await client.get(f'standings/division/{division_id}', params=[('season', str(season))])
if data and isinstance(data, list):
logger.debug(f"Retrieved division {division_id} standings for season {season}: {len(data)} teams")
return data
logger.debug(f"No division standings found for division {division_id}, season {season}")
return None
except Exception as e:
logger.error(f"Failed to get division {division_id} standings: {e}")
return None
async def get_league_leaders(self, stat_type: str = 'batting', season: Optional[int] = None, limit: int = 10) -> Optional[List[Dict[str, Any]]]:
"""
Get league leaders for a specific statistic category.
Args:
stat_type: Type of stats ('batting', 'pitching', 'fielding')
season: Season number (defaults to current season)
limit: Number of leaders to return
Returns:
List of league leaders or None if not available
"""
try:
season = season or get_config().sba_season
client = await self.get_client()
params = [
('season', str(season)),
('limit', str(limit))
]
data = await client.get(f'leaders/{stat_type}', params=params)
if data:
# Handle different response formats
if isinstance(data, list):
leaders = data
elif isinstance(data, dict):
leaders = data.get('leaders', data.get('items', data.get('results', [])))
else:
leaders = []
logger.debug(f"Retrieved {stat_type} leaders for season {season}: {len(leaders)} players")
return leaders[:limit] # Ensure we don't exceed limit
logger.debug(f"No {stat_type} leaders found for season {season}")
return None
except Exception as e:
logger.error(f"Failed to get {stat_type} leaders for season {season}: {e}")
return None
# Global service instance
league_service = LeagueService()