major-domo-v2/services/game_service.py
Cal Corum 2409c27c1d CLAUDE: Add comprehensive scorecard submission system
Implements full Google Sheets scorecard submission with:
- Complete game data extraction (68 play fields, pitching decisions, box score)
- Transaction rollback support at 3 states (plays/game/complete)
- Duplicate game detection with confirmation dialog
- Permission-based submission (GMs only)
- Automated results posting to news channel
- Automatic standings recalculation
- Key plays display with WPA sorting

New Components:
- Play, Decision, Game models with full validation
- SheetsService for Google Sheets integration
- GameService, PlayService, DecisionService for data management
- ConfirmationView for user confirmations
- Discord helper utilities for channel operations

Services Enhanced:
- StandingsService: Added recalculate_standings() method
- CustomCommandsService: Fixed creator endpoint path
- Team/Player models: Added helper methods for display

Configuration:
- Added SHEETS_CREDENTIALS_PATH environment variable
- Added SBA_NETWORK_NEWS_CHANNEL and role constants
- Enabled pygsheets dependency

Documentation:
- Comprehensive README updates across all modules
- Added command, service, model, and view documentation
- Detailed workflow and error handling documentation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 00:21:32 -05:00

183 lines
5.0 KiB
Python

"""
Game Service
Manages game CRUD operations and game-specific workflows for scorecard submission.
"""
from typing import Optional
from services.base_service import BaseService
from models.game import Game
from utils.logging import get_contextual_logger
from exceptions import APIException
class GameService(BaseService[Game]):
"""Game management service with specialized game operations."""
def __init__(self):
"""Initialize game service."""
super().__init__(Game, 'games')
self.logger = get_contextual_logger(f'{__name__}.GameService')
async def find_duplicate_game(
self,
season: int,
week: int,
game_num: int,
away_team_id: int,
home_team_id: int
) -> Optional[Game]:
"""
Check for already-played duplicate game.
Args:
season: Season number
week: Week number
game_num: Game number in series
away_team_id: Away team ID
home_team_id: Home team ID
Returns:
Game if duplicate found (game_num is set), None otherwise
"""
params = [
('season', str(season)),
('week', str(week)),
('game_num', str(game_num)),
('away_team_id', str(away_team_id)),
('home_team_id', str(home_team_id))
]
games, count = await self.get_all(params=params)
if count > 0:
self.logger.warning(
f"Found duplicate game: S{season} W{week} G{game_num} "
f"({away_team_id} @ {home_team_id})"
)
return games[0]
return None
async def find_scheduled_game(
self,
season: int,
week: int,
away_team_id: int,
home_team_id: int
) -> Optional[Game]:
"""
Find unplayed scheduled game matching teams and week.
Args:
season: Season number
week: Week number
away_team_id: Away team ID
home_team_id: Home team ID
Returns:
Game if found and not yet played (game_num is None), None otherwise
"""
params = [
('season', str(season)),
('week', str(week)),
('away_team_id', str(away_team_id)),
('home_team_id', str(home_team_id)),
('played', 'false') # Only unplayed games
]
games, count = await self.get_all(params=params)
if count == 0:
self.logger.warning(
f"No scheduled game found for S{season} W{week} "
f"({away_team_id} @ {home_team_id})"
)
return None
return games[0]
async def wipe_game_data(self, game_id: int) -> bool:
"""
Wipe game scores and manager assignments.
Calls POST /games/wipe/{game_id} which sets:
- away_score = None
- home_score = None
- game_num = None
- away_manager = None
- home_manager = None
Args:
game_id: Game ID to wipe
Returns:
True if successful
Raises:
APIException: If wipe fails
"""
try:
client = await self.get_client()
response = await client.post(f'games/wipe/{game_id}', {})
self.logger.info(f"Wiped game {game_id}")
return True
except Exception as e:
self.logger.error(f"Failed to wipe game {game_id}: {e}")
raise APIException(f"Failed to wipe game data: {e}")
async def update_game_result(
self,
game_id: int,
away_score: int,
home_score: int,
away_manager_id: int,
home_manager_id: int,
game_num: int,
scorecard_url: str
) -> Game:
"""
Update game with scores, managers, and scorecard URL.
Args:
game_id: Game ID to update
away_score: Away team final score
home_score: Home team final score
away_manager_id: Away team manager ID
home_manager_id: Home team manager ID
game_num: Game number in series
scorecard_url: URL to scorecard
Returns:
Updated game object
Raises:
APIException: If update fails
"""
update_data = {
'away_score': away_score,
'home_score': home_score,
'away_manager_id': away_manager_id,
'home_manager_id': home_manager_id,
'game_num': game_num,
'scorecard_url': scorecard_url
}
updated_game = await self.patch(
game_id,
update_data,
use_query_params=True # API expects query params for PATCH
)
if updated_game is None:
raise APIException(f"Game {game_id} not found for update")
self.logger.info(f"Updated game {game_id} with final score")
return updated_game
# Global service instance
game_service = GameService()