major-domo-v2/models/team.py
Cal Corum 758be0f166 CLAUDE: Fix trade system issues and enhance documentation
Major fixes and improvements:

Trade System Fixes:
- Fix duplicate player moves in trade embed Player Exchanges section
- Resolve "WVMiL not participating" error for Minor League destinations
- Implement organizational authority model for ML/MiL/IL team relationships
- Update Trade.cross_team_moves to deduplicate using moves_giving only

Team Model Enhancements:
- Rewrite roster_type() method using sname as definitive source per spec
- Fix edge cases like "BHMIL" (Birmingham IL) vs "BHMMIL"
- Update _get_base_abbrev() to use consistent sname-based logic
- Add organizational lookup support in trade participation

Autocomplete System:
- Fix major_league_team_autocomplete invalid roster_type parameter
- Implement client-side filtering using Team.roster_type() method
- Add comprehensive test coverage for all autocomplete functions
- Centralize autocomplete logic to shared utils functions

Test Infrastructure:
- Add 25 new tests for trade models and trade builder
- Add 13 autocomplete function tests with error handling
- Fix existing test failures with proper mocking patterns
- Update dropadd tests to use shared autocomplete functions

Documentation Updates:
- Document trade model enhancements and deduplication fix
- Add autocomplete function documentation with usage examples
- Document organizational authority model and edge case handling
- Update README files with recent fixes and implementation notes

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

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

200 lines
7.3 KiB
Python

"""
Team model for SBA teams
Represents a team in the league with all associated metadata.
"""
from typing import Optional
from enum import Enum
from pydantic import Field
from models.base import SBABaseModel
from models.division import Division
class RosterType(Enum):
"""Roster designation types."""
MAJOR_LEAGUE = "ml"
MINOR_LEAGUE = "mil"
INJURED_LIST = "il"
FREE_AGENCY = "fa"
class Team(SBABaseModel):
"""Team model representing an SBA team."""
# Override base model to make id required for database entities
id: int = Field(..., description="Team ID from database")
abbrev: str = Field(..., description="Team abbreviation (e.g., 'NYY')")
sname: str = Field(..., description="Short team name")
lname: str = Field(..., description="Long team name")
season: int = Field(..., description="Season number")
# Manager information
gmid: Optional[int] = Field(None, description="Primary general manager ID")
gmid2: Optional[int] = Field(None, description="Secondary general manager ID")
manager1_id: Optional[int] = Field(None, description="Primary manager ID")
manager2_id: Optional[int] = Field(None, description="Secondary manager ID")
# Team metadata
division_id: Optional[int] = Field(None, description="Division ID")
division: Optional[Division] = Field(None, description="Division object (populated from API)")
stadium: Optional[str] = Field(None, description="Home stadium name")
thumbnail: Optional[str] = Field(None, description="Team thumbnail URL")
color: Optional[str] = Field(None, description="Primary team color")
dice_color: Optional[str] = Field(None, description="Dice rolling color")
@classmethod
def from_api_data(cls, data: dict) -> 'Team':
"""
Create Team instance from API data, handling nested division structure.
The API returns division data as a nested object, but our model expects
both division_id (int) and division (optional Division object).
"""
# Make a copy to avoid modifying original data
team_data = data.copy()
# Handle nested division structure
if 'division' in team_data and isinstance(team_data['division'], dict):
division_data = team_data['division']
# Extract division_id from nested division object
team_data['division_id'] = division_data.get('id')
# Keep division object for optional population
if division_data.get('id'):
team_data['division'] = Division.from_api_data(division_data)
return super().from_api_data(team_data)
def roster_type(self) -> RosterType:
"""Determine the roster type based on team abbreviation and name."""
if len(self.abbrev) <= 3:
return RosterType.MAJOR_LEAGUE
# Use sname as the definitive source of truth for IL teams
# If "IL" is in sname and abbrev ends in "IL" → Injured List
if self.abbrev.upper().endswith('IL') and 'IL' in self.sname:
return RosterType.INJURED_LIST
# If abbrev ends with "MiL" (exact case) and "IL" not in sname → Minor League
if self.abbrev.endswith('MiL') and 'IL' not in self.sname:
return RosterType.MINOR_LEAGUE
# Handle other patterns
abbrev_lower = self.abbrev.lower()
if abbrev_lower.endswith('mil'):
return RosterType.MINOR_LEAGUE
elif abbrev_lower.endswith('il'):
return RosterType.INJURED_LIST
else:
return RosterType.MAJOR_LEAGUE
def _get_base_abbrev(self) -> str:
"""
Extract the base team abbreviation from potentially extended abbreviation.
Returns:
Base team abbreviation (typically 3 characters)
"""
abbrev_lower = self.abbrev.lower()
# If 3 chars or less, it's already the base team
if len(self.abbrev) <= 3:
return self.abbrev
# Handle teams ending in 'mil' - use sname to determine if IL or MiL
if abbrev_lower.endswith('mil'):
# If "IL" is in sname and abbrev ends in "IL" → It's [Team]IL
if self.abbrev.upper().endswith('IL') and 'IL' in self.sname:
return self.abbrev[:-2] # Remove 'IL'
# Otherwise it's minor league → remove 'MIL'
return self.abbrev[:-3]
# Handle injured list: ends with 'il' but not 'mil'
if abbrev_lower.endswith('il'):
return self.abbrev[:-2] # Remove 'IL'
# Unknown pattern, return as-is
return self.abbrev
async def major_league_affiliate(self) -> 'Team':
"""
Get the major league team for this organization via API call.
Returns:
Team instance representing the major league affiliate
Raises:
APIException: If the affiliate team cannot be found
"""
from services.team_service import team_service
base_abbrev = self._get_base_abbrev()
if base_abbrev == self.abbrev:
return self # Already the major league team
team = await team_service.get_team_by_abbrev(base_abbrev, self.season)
if team is None:
raise ValueError(f"Major league affiliate not found for team {self.abbrev} (looking for {base_abbrev})")
return team
async def minor_league_affiliate(self) -> 'Team':
"""
Get the minor league team for this organization via API call.
Returns:
Team instance representing the minor league affiliate
Raises:
APIException: If the affiliate team cannot be found
"""
from services.team_service import team_service
base_abbrev = self._get_base_abbrev()
mil_abbrev = f"{base_abbrev}MIL"
if mil_abbrev == self.abbrev:
return self # Already the minor league team
team = await team_service.get_team_by_abbrev(mil_abbrev, self.season)
if team is None:
raise ValueError(f"Minor league affiliate not found for team {self.abbrev} (looking for {mil_abbrev})")
return team
async def injured_list_affiliate(self) -> 'Team':
"""
Get the injured list team for this organization via API call.
Returns:
Team instance representing the injured list affiliate
Raises:
APIException: If the affiliate team cannot be found
"""
from services.team_service import team_service
base_abbrev = self._get_base_abbrev()
il_abbrev = f"{base_abbrev}IL"
if il_abbrev == self.abbrev:
return self # Already the injured list team
team = await team_service.get_team_by_abbrev(il_abbrev, self.season)
if team is None:
raise ValueError(f"Injured list affiliate not found for team {self.abbrev} (looking for {il_abbrev})")
return team
def is_same_organization(self, other_team: 'Team') -> bool:
"""
Check if this team and another team are from the same organization.
Args:
other_team: Another team to compare
Returns:
True if both teams are from the same organization
"""
return self._get_base_abbrev() == other_team._get_base_abbrev()
def __str__(self):
return f"{self.abbrev} - {self.lname}"