fix: prefix trade validation errors with team abbreviation
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m5s
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m5s
Roster-level errors, warnings, and suggestions now display as "[BSG] Too many ML players" so users know which team each issue applies to. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f7a65706a1
commit
8ee35e564c
@ -24,6 +24,7 @@ class TradeValidationResult:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.is_legal: bool = True
|
self.is_legal: bool = True
|
||||||
self.participant_validations: Dict[int, RosterValidationResult] = {}
|
self.participant_validations: Dict[int, RosterValidationResult] = {}
|
||||||
|
self.team_abbrevs: Dict[int, str] = {} # team_id -> abbreviation
|
||||||
self.trade_errors: List[str] = []
|
self.trade_errors: List[str] = []
|
||||||
self.trade_warnings: List[str] = []
|
self.trade_warnings: List[str] = []
|
||||||
self.trade_suggestions: List[str] = []
|
self.trade_suggestions: List[str] = []
|
||||||
@ -32,24 +33,30 @@ class TradeValidationResult:
|
|||||||
def all_errors(self) -> List[str]:
|
def all_errors(self) -> List[str]:
|
||||||
"""Get all errors including trade-level and roster-level errors."""
|
"""Get all errors including trade-level and roster-level errors."""
|
||||||
errors = self.trade_errors.copy()
|
errors = self.trade_errors.copy()
|
||||||
for validation in self.participant_validations.values():
|
for team_id, validation in self.participant_validations.items():
|
||||||
errors.extend(validation.errors)
|
abbrev = self.team_abbrevs.get(team_id, "???")
|
||||||
|
for error in validation.errors:
|
||||||
|
errors.append(f"[{abbrev}] {error}")
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_warnings(self) -> List[str]:
|
def all_warnings(self) -> List[str]:
|
||||||
"""Get all warnings across trade and roster levels."""
|
"""Get all warnings across trade and roster levels."""
|
||||||
warnings = self.trade_warnings.copy()
|
warnings = self.trade_warnings.copy()
|
||||||
for validation in self.participant_validations.values():
|
for team_id, validation in self.participant_validations.items():
|
||||||
warnings.extend(validation.warnings)
|
abbrev = self.team_abbrevs.get(team_id, "???")
|
||||||
|
for warning in validation.warnings:
|
||||||
|
warnings.append(f"[{abbrev}] {warning}")
|
||||||
return warnings
|
return warnings
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_suggestions(self) -> List[str]:
|
def all_suggestions(self) -> List[str]:
|
||||||
"""Get all suggestions across trade and roster levels."""
|
"""Get all suggestions across trade and roster levels."""
|
||||||
suggestions = self.trade_suggestions.copy()
|
suggestions = self.trade_suggestions.copy()
|
||||||
for validation in self.participant_validations.values():
|
for team_id, validation in self.participant_validations.items():
|
||||||
suggestions.extend(validation.suggestions)
|
abbrev = self.team_abbrevs.get(team_id, "???")
|
||||||
|
for suggestion in validation.suggestions:
|
||||||
|
suggestions.append(f"[{abbrev}] {suggestion}")
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
def get_participant_validation(self, team_id: int) -> Optional[RosterValidationResult]:
|
def get_participant_validation(self, team_id: int) -> Optional[RosterValidationResult]:
|
||||||
@ -456,6 +463,7 @@ class TradeBuilder:
|
|||||||
# Validate each team's roster after the trade
|
# Validate each team's roster after the trade
|
||||||
for participant in self.trade.participants:
|
for participant in self.trade.participants:
|
||||||
team_id = participant.team.id
|
team_id = participant.team.id
|
||||||
|
result.team_abbrevs[team_id] = participant.team.abbrev
|
||||||
if team_id in self._team_builders:
|
if team_id in self._team_builders:
|
||||||
builder = self._team_builders[team_id]
|
builder = self._team_builders[team_id]
|
||||||
roster_validation = await builder.validate_transaction(next_week)
|
roster_validation = await builder.validate_transaction(next_week)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ Tests for trade builder service.
|
|||||||
Tests the TradeBuilder service functionality including multi-team management,
|
Tests the TradeBuilder service functionality including multi-team management,
|
||||||
move validation, and trade validation logic.
|
move validation, and trade validation logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ class TestTradeBuilder:
|
|||||||
self.player1.team_id = self.team1.id
|
self.player1.team_id = self.team1.id
|
||||||
|
|
||||||
# Mock team_service to return team1 for this player
|
# Mock team_service to return team1 for this player
|
||||||
with patch('services.trade_builder.team_service') as mock_team_service:
|
with patch("services.trade_builder.team_service") as mock_team_service:
|
||||||
mock_team_service.get_team = AsyncMock(return_value=self.team1)
|
mock_team_service.get_team = AsyncMock(return_value=self.team1)
|
||||||
|
|
||||||
# Don't mock is_same_organization - let the real method work
|
# Don't mock is_same_organization - let the real method work
|
||||||
@ -119,7 +120,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert success
|
assert success
|
||||||
@ -141,7 +142,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team2,
|
from_team=self.team2,
|
||||||
to_team=self.team1,
|
to_team=self.team1,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not success
|
assert not success
|
||||||
@ -156,9 +157,7 @@ class TestTradeBuilder:
|
|||||||
|
|
||||||
# Create a player on Free Agency
|
# Create a player on Free Agency
|
||||||
fa_player = PlayerFactory.create(
|
fa_player = PlayerFactory.create(
|
||||||
id=100,
|
id=100, name="FA Player", team_id=get_config().free_agent_team_id
|
||||||
name="FA Player",
|
|
||||||
team_id=get_config().free_agent_team_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to add player from FA (should fail)
|
# Try to add player from FA (should fail)
|
||||||
@ -167,7 +166,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not success
|
assert not success
|
||||||
@ -182,9 +181,7 @@ class TestTradeBuilder:
|
|||||||
|
|
||||||
# Create a player without a team
|
# Create a player without a team
|
||||||
no_team_player = PlayerFactory.create(
|
no_team_player = PlayerFactory.create(
|
||||||
id=101,
|
id=101, name="No Team Player", team_id=None
|
||||||
name="No Team Player",
|
|
||||||
team_id=None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to add player without team (should fail)
|
# Try to add player without team (should fail)
|
||||||
@ -193,7 +190,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not success
|
assert not success
|
||||||
@ -208,24 +205,22 @@ class TestTradeBuilder:
|
|||||||
|
|
||||||
# Create a player on team3 (not in trade)
|
# Create a player on team3 (not in trade)
|
||||||
player_on_team3 = PlayerFactory.create(
|
player_on_team3 = PlayerFactory.create(
|
||||||
id=102,
|
id=102, name="Team3 Player", team_id=self.team3.id
|
||||||
name="Team3 Player",
|
|
||||||
team_id=self.team3.id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mock team_service to return team3 for this player
|
# Mock team_service to return team3 for this player
|
||||||
with patch('services.trade_builder.team_service') as mock_team_service:
|
with patch("services.trade_builder.team_service") as mock_team_service:
|
||||||
mock_team_service.get_team = AsyncMock(return_value=self.team3)
|
mock_team_service.get_team = AsyncMock(return_value=self.team3)
|
||||||
|
|
||||||
# Mock is_same_organization to return False (different organization, sync method)
|
# Mock is_same_organization to return False (different organization, sync method)
|
||||||
with patch('models.team.Team.is_same_organization', return_value=False):
|
with patch("models.team.Team.is_same_organization", return_value=False):
|
||||||
# Try to add player from team3 claiming it's from team1 (should fail)
|
# Try to add player from team3 claiming it's from team1 (should fail)
|
||||||
success, error = await builder.add_player_move(
|
success, error = await builder.add_player_move(
|
||||||
player=player_on_team3,
|
player=player_on_team3,
|
||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not success
|
assert not success
|
||||||
@ -241,26 +236,24 @@ class TestTradeBuilder:
|
|||||||
|
|
||||||
# Create a player on team1's minor league affiliate
|
# Create a player on team1's minor league affiliate
|
||||||
player_on_team1_mil = PlayerFactory.create(
|
player_on_team1_mil = PlayerFactory.create(
|
||||||
id=103,
|
id=103, name="Team1 MiL Player", team_id=999 # Some MiL team ID
|
||||||
name="Team1 MiL Player",
|
|
||||||
team_id=999 # Some MiL team ID
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mock team_service to return the MiL team
|
# Mock team_service to return the MiL team
|
||||||
mil_team = TeamFactory.create(id=999, abbrev="WVMiL", sname="West Virginia MiL")
|
mil_team = TeamFactory.create(id=999, abbrev="WVMiL", sname="West Virginia MiL")
|
||||||
|
|
||||||
with patch('services.trade_builder.team_service') as mock_team_service:
|
with patch("services.trade_builder.team_service") as mock_team_service:
|
||||||
mock_team_service.get_team = AsyncMock(return_value=mil_team)
|
mock_team_service.get_team = AsyncMock(return_value=mil_team)
|
||||||
|
|
||||||
# Mock is_same_organization to return True (same organization, sync method)
|
# Mock is_same_organization to return True (same organization, sync method)
|
||||||
with patch('models.team.Team.is_same_organization', return_value=True):
|
with patch("models.team.Team.is_same_organization", return_value=True):
|
||||||
# Add player from WVMiL (should succeed because it's same organization as WV)
|
# Add player from WVMiL (should succeed because it's same organization as WV)
|
||||||
success, error = await builder.add_player_move(
|
success, error = await builder.add_player_move(
|
||||||
player=player_on_team1_mil,
|
player=player_on_team1_mil,
|
||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MINOR_LEAGUE,
|
from_roster=RosterType.MINOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert success
|
assert success
|
||||||
@ -278,7 +271,7 @@ class TestTradeBuilder:
|
|||||||
team=self.team1,
|
team=self.team1,
|
||||||
player=self.player1,
|
player=self.player1,
|
||||||
from_roster=RosterType.MINOR_LEAGUE,
|
from_roster=RosterType.MINOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert success
|
assert success
|
||||||
@ -293,7 +286,7 @@ class TestTradeBuilder:
|
|||||||
team=self.team3,
|
team=self.team3,
|
||||||
player=self.player2,
|
player=self.player2,
|
||||||
from_roster=RosterType.MINOR_LEAGUE,
|
from_roster=RosterType.MINOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not success
|
assert not success
|
||||||
@ -309,7 +302,7 @@ class TestTradeBuilder:
|
|||||||
self.player1.team_id = self.team1.id
|
self.player1.team_id = self.team1.id
|
||||||
|
|
||||||
# Mock team_service for adding the player
|
# Mock team_service for adding the player
|
||||||
with patch('services.trade_builder.team_service') as mock_team_service:
|
with patch("services.trade_builder.team_service") as mock_team_service:
|
||||||
mock_team_service.get_team = AsyncMock(return_value=self.team1)
|
mock_team_service.get_team = AsyncMock(return_value=self.team1)
|
||||||
|
|
||||||
# Add a player move
|
# Add a player move
|
||||||
@ -318,7 +311,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not builder.is_empty
|
assert not builder.is_empty
|
||||||
@ -347,7 +340,7 @@ class TestTradeBuilder:
|
|||||||
await builder.add_team(self.team2)
|
await builder.add_team(self.team2)
|
||||||
|
|
||||||
# Mock the transaction builders
|
# Mock the transaction builders
|
||||||
with patch.object(builder, '_get_or_create_builder') as mock_get_builder:
|
with patch.object(builder, "_get_or_create_builder") as mock_get_builder:
|
||||||
mock_builder1 = MagicMock()
|
mock_builder1 = MagicMock()
|
||||||
mock_builder2 = MagicMock()
|
mock_builder2 = MagicMock()
|
||||||
|
|
||||||
@ -360,7 +353,7 @@ class TestTradeBuilder:
|
|||||||
minor_league_count=5,
|
minor_league_count=5,
|
||||||
warnings=[],
|
warnings=[],
|
||||||
errors=[],
|
errors=[],
|
||||||
suggestions=[]
|
suggestions=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_builder1.validate_transaction = AsyncMock(return_value=valid_result)
|
mock_builder1.validate_transaction = AsyncMock(return_value=valid_result)
|
||||||
@ -391,7 +384,7 @@ class TestTradeBuilder:
|
|||||||
await builder.add_team(self.team2)
|
await builder.add_team(self.team2)
|
||||||
|
|
||||||
# Mock the transaction builders
|
# Mock the transaction builders
|
||||||
with patch.object(builder, '_get_or_create_builder') as mock_get_builder:
|
with patch.object(builder, "_get_or_create_builder") as mock_get_builder:
|
||||||
mock_builder1 = MagicMock()
|
mock_builder1 = MagicMock()
|
||||||
mock_builder2 = MagicMock()
|
mock_builder2 = MagicMock()
|
||||||
|
|
||||||
@ -404,7 +397,7 @@ class TestTradeBuilder:
|
|||||||
minor_league_count=5,
|
minor_league_count=5,
|
||||||
warnings=[],
|
warnings=[],
|
||||||
errors=[],
|
errors=[],
|
||||||
suggestions=[]
|
suggestions=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_builder1.validate_transaction = AsyncMock(return_value=valid_result)
|
mock_builder1.validate_transaction = AsyncMock(return_value=valid_result)
|
||||||
@ -440,7 +433,7 @@ class TestTradeBuilder:
|
|||||||
return self.team2
|
return self.team2
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with patch('services.trade_builder.team_service') as mock_team_service:
|
with patch("services.trade_builder.team_service") as mock_team_service:
|
||||||
mock_team_service.get_team = AsyncMock(side_effect=get_team_side_effect)
|
mock_team_service.get_team = AsyncMock(side_effect=get_team_side_effect)
|
||||||
|
|
||||||
# Add balanced moves - no need to mock is_same_organization
|
# Add balanced moves - no need to mock is_same_organization
|
||||||
@ -449,7 +442,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team1,
|
from_team=self.team1,
|
||||||
to_team=self.team2,
|
to_team=self.team2,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
await builder.add_player_move(
|
await builder.add_player_move(
|
||||||
@ -457,7 +450,7 @@ class TestTradeBuilder:
|
|||||||
from_team=self.team2,
|
from_team=self.team2,
|
||||||
to_team=self.team1,
|
to_team=self.team1,
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate balanced trade
|
# Validate balanced trade
|
||||||
@ -829,7 +822,9 @@ class TestTradeBuilderCache:
|
|||||||
"""
|
"""
|
||||||
user_id = 12345
|
user_id = 12345
|
||||||
team1 = TeamFactory.west_virginia()
|
team1 = TeamFactory.west_virginia()
|
||||||
team3 = TeamFactory.create(id=999, abbrev="POR", name="Portland") # Non-participant
|
team3 = TeamFactory.create(
|
||||||
|
id=999, abbrev="POR", name="Portland"
|
||||||
|
) # Non-participant
|
||||||
|
|
||||||
# Create builder with team1
|
# Create builder with team1
|
||||||
get_trade_builder(user_id, team1)
|
get_trade_builder(user_id, team1)
|
||||||
@ -955,7 +950,9 @@ class TestTradeBuilderCache:
|
|||||||
|
|
||||||
This ensures proper error handling when a GM not in the trade tries to clear it.
|
This ensures proper error handling when a GM not in the trade tries to clear it.
|
||||||
"""
|
"""
|
||||||
team3 = TeamFactory.create(id=999, abbrev="POR", name="Portland") # Non-participant
|
team3 = TeamFactory.create(
|
||||||
|
id=999, abbrev="POR", name="Portland"
|
||||||
|
) # Non-participant
|
||||||
|
|
||||||
result = clear_trade_builder_by_team(team3.id)
|
result = clear_trade_builder_by_team(team3.id)
|
||||||
assert result is False
|
assert result is False
|
||||||
@ -982,7 +979,7 @@ class TestTradeValidationResult:
|
|||||||
minor_league_count=5,
|
minor_league_count=5,
|
||||||
warnings=["Team1 warning"],
|
warnings=["Team1 warning"],
|
||||||
errors=["Team1 error"],
|
errors=["Team1 error"],
|
||||||
suggestions=["Team1 suggestion"]
|
suggestions=["Team1 suggestion"],
|
||||||
)
|
)
|
||||||
|
|
||||||
team2_validation = RosterValidationResult(
|
team2_validation = RosterValidationResult(
|
||||||
@ -991,28 +988,30 @@ class TestTradeValidationResult:
|
|||||||
minor_league_count=4,
|
minor_league_count=4,
|
||||||
warnings=[],
|
warnings=[],
|
||||||
errors=[],
|
errors=[],
|
||||||
suggestions=[]
|
suggestions=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
result.participant_validations[1] = team1_validation
|
result.participant_validations[1] = team1_validation
|
||||||
result.participant_validations[2] = team2_validation
|
result.participant_validations[2] = team2_validation
|
||||||
|
result.team_abbrevs[1] = "TM1"
|
||||||
|
result.team_abbrevs[2] = "TM2"
|
||||||
result.is_legal = False # One team has errors
|
result.is_legal = False # One team has errors
|
||||||
|
|
||||||
# Test aggregated results
|
# Test aggregated results - roster errors are prefixed with team abbrev
|
||||||
all_errors = result.all_errors
|
all_errors = result.all_errors
|
||||||
assert len(all_errors) == 3 # 2 trade + 1 team
|
assert len(all_errors) == 3 # 2 trade + 1 team
|
||||||
assert "Trade error 1" in all_errors
|
assert "Trade error 1" in all_errors
|
||||||
assert "Team1 error" in all_errors
|
assert "[TM1] Team1 error" in all_errors
|
||||||
|
|
||||||
all_warnings = result.all_warnings
|
all_warnings = result.all_warnings
|
||||||
assert len(all_warnings) == 2 # 1 trade + 1 team
|
assert len(all_warnings) == 2 # 1 trade + 1 team
|
||||||
assert "Trade warning 1" in all_warnings
|
assert "Trade warning 1" in all_warnings
|
||||||
assert "Team1 warning" in all_warnings
|
assert "[TM1] Team1 warning" in all_warnings
|
||||||
|
|
||||||
all_suggestions = result.all_suggestions
|
all_suggestions = result.all_suggestions
|
||||||
assert len(all_suggestions) == 2 # 1 trade + 1 team
|
assert len(all_suggestions) == 2 # 1 trade + 1 team
|
||||||
assert "Trade suggestion 1" in all_suggestions
|
assert "Trade suggestion 1" in all_suggestions
|
||||||
assert "Team1 suggestion" in all_suggestions
|
assert "[TM1] Team1 suggestion" in all_suggestions
|
||||||
|
|
||||||
# Test participant validation lookup
|
# Test participant validation lookup
|
||||||
team1_val = result.get_participant_validation(1)
|
team1_val = result.get_participant_validation(1)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user