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>
215 lines
7.7 KiB
Python
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() |