paper-dynasty-gameplay-webapp/app/services/player_service.py
Cal Corum 559fe73f07 CLAUDE: Complete Player model migration with service layer and integration test infrastructure
## Player Model Migration
- Migrate Player model from Discord app following Model/Service Architecture pattern
- Extract all business logic from Player model to PlayerService
- Create pure data model with PostgreSQL relationships (Cardset, PositionRating)
- Implement comprehensive PlayerFactory with specialized methods for test data

## PlayerService Implementation
- Extract 5 business logic methods from original Player model:
  - get_batter_card_url() - batting card URL retrieval
  - get_pitcher_card_url() - pitching card URL retrieval
  - generate_name_card_link() - markdown link generation
  - get_formatted_name_with_description() - name formatting
  - get_player_description() - description from object or dict
- Follow BaseService pattern with dependency injection and logging

## Comprehensive Testing
- 35 passing Player tests (14 model + 21 service tests)
- PlayerFactory with specialized methods (batting/pitching cards, positions)
- Test isolation following factory pattern and db_session guidelines
- Fix PostgreSQL integer overflow in test ID generation

## Integration Test Infrastructure
- Create integration test framework for improving service coverage
- Design AIService integration tests targeting uncovered branches
- Demonstrate real database query testing with proper isolation
- Establish patterns for testing complex game scenarios

## Service Coverage Analysis
- Current service coverage: 61% overall
- PlayerService: 100% coverage (excellent migration example)
- AIService: 60% coverage (improvement opportunities identified)
- Integration test strategy designed to achieve 90%+ coverage

## Database Integration
- Update Cardset model to include players relationship
- Update PositionRating model with proper Player foreign key
- Maintain all existing relationships and constraints
- Demonstrate data isolation and automatic cleanup in tests

## Test Suite Status
- 137 tests passing, 0 failures (maintained 100% pass rate)
- Added 35 new tests while preserving all existing functionality
- Integration test infrastructure ready for coverage improvements

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 16:20:29 -05:00

153 lines
4.8 KiB
Python

"""
Player Service
Business logic for player-related operations, extracted from Player model.
Handles URL generation, formatting, and player description logic.
"""
import logging
from typing import Dict, Literal, Optional, Union
from sqlmodel import Session
from ..models.player import Player
from .base_service import BaseService
class PlayerService(BaseService):
"""Service for player-related business logic."""
def __init__(self, session: Session):
super().__init__(session)
self.logger = logging.getLogger(f'{__name__}.{self.__class__.__name__}')
def get_batter_card_url(self, player: Player) -> Optional[str]:
"""
Get the batting card image URL for a player.
Migrated from Player.batter_card_url property.
Args:
player: Player instance
Returns:
str: Batting card URL if found, None otherwise
"""
self._log_operation("get_batter_card_url", f"player {player.name}")
if player.image and 'batting' in player.image:
return player.image
elif player.image2 and 'batting' in player.image2:
return player.image2
else:
return None
def get_pitcher_card_url(self, player: Player) -> Optional[str]:
"""
Get the pitching card image URL for a player.
Migrated from Player.pitcher_card_url property.
Args:
player: Player instance
Returns:
str: Pitching card URL if found, None otherwise
"""
self._log_operation("get_pitcher_card_url", f"player {player.name}")
if player.image and 'pitching' in player.image:
return player.image
elif player.image2 and 'pitching' in player.image2:
return player.image2
else:
return None
def generate_name_card_link(self, player: Player, card_type: Literal['pitching', 'batting']) -> str:
"""
Generate a markdown link with player name and card URL.
Migrated from Player.name_card_link() method.
Args:
player: Player instance
card_type: Type of card ('pitching' or 'batting')
Returns:
str: Markdown formatted link
Raises:
ValueError: If card URL is not available for the specified type
"""
self._log_operation("generate_name_card_link", f"player {player.name}, type {card_type}")
if card_type == 'pitching':
url = self.get_pitcher_card_url(player)
else:
url = self.get_batter_card_url(player)
if url is None:
raise ValueError(f"No {card_type} card URL available for player {player.name}")
return f'[{player.name}]({url})'
def get_formatted_name_with_description(self, player: Player) -> str:
"""
Get formatted player name with description.
Migrated from Player.name_with_desc property.
Args:
player: Player instance
Returns:
str: Formatted name with description
"""
self._log_operation("get_formatted_name_with_description", f"player {player.name}")
return f'{player.description} {player.name}'
def get_player_description(
self,
player: Optional[Player] = None,
player_dict: Optional[Dict[str, Union[str, int]]] = None
) -> str:
"""
Get full player description from Player object or dictionary.
Migrated from standalone player_description() function.
Args:
player: Player instance (optional)
player_dict: Dictionary with player data (optional)
Returns:
str: Full player description
Raises:
TypeError: If neither player nor player_dict is provided
KeyError: If required keys are missing from player_dict
"""
if player is None and player_dict is None:
err = 'One of "player" or "player_dict" must be included to get full description'
self._log_error("get_player_description", err)
raise TypeError(err)
if player is not None:
self._log_operation("get_player_description", f"from Player object: {player.name}")
return f'{player.description} {player.name}'
# Handle dictionary case
if 'description' not in player_dict:
err = 'player_dict must contain "description" key'
self._log_error("get_player_description", err)
raise KeyError(err)
r_val = f'{player_dict["description"]}'
if 'name' in player_dict:
r_val += f' {player_dict["name"]}'
elif 'p_name' in player_dict:
r_val += f' {player_dict["p_name"]}'
self._log_operation("get_player_description", f"from dict: {r_val}")
return r_val