Fix sWAR cap validation for /ilmove and /dropadd commands
- Add sWAR cap validation to TransactionBuilder.validate_transaction() - Use team-specific salary_cap from Team.salary_cap field - Fall back to config.swar_cap_limit (32.0) if team has no custom cap - Add major_league_swar_cap field to RosterValidationResult - Update major_league_swar_status to show ✅/❌ with cap limit - Add 4 new tests for sWAR cap validation This fixes a bug where IL moves could put a team over their sWAR cap because the validation only checked roster counts, not sWAR limits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4bdfe3ee0a
commit
889b3a4e2d
@ -87,6 +87,7 @@ class RosterValidationResult:
|
|||||||
minor_league_limit: int = 6
|
minor_league_limit: int = 6
|
||||||
major_league_swar: float = 0.0
|
major_league_swar: float = 0.0
|
||||||
minor_league_swar: float = 0.0
|
minor_league_swar: float = 0.0
|
||||||
|
major_league_swar_cap: float = 32.0 # Team's sWAR cap limit
|
||||||
pre_existing_ml_swar_change: float = 0.0
|
pre_existing_ml_swar_change: float = 0.0
|
||||||
pre_existing_mil_swar_change: float = 0.0
|
pre_existing_mil_swar_change: float = 0.0
|
||||||
pre_existing_transaction_count: int = 0
|
pre_existing_transaction_count: int = 0
|
||||||
@ -110,7 +111,10 @@ class RosterValidationResult:
|
|||||||
@property
|
@property
|
||||||
def major_league_swar_status(self) -> str:
|
def major_league_swar_status(self) -> str:
|
||||||
"""Status string for major league sWAR."""
|
"""Status string for major league sWAR."""
|
||||||
return f"📊 Major League sWAR: {self.major_league_swar:.2f}"
|
if self.major_league_swar > self.major_league_swar_cap:
|
||||||
|
return f"❌ Major League sWAR: {self.major_league_swar:.2f}/{self.major_league_swar_cap:.1f} (Over cap!)"
|
||||||
|
else:
|
||||||
|
return f"✅ Major League sWAR: {self.major_league_swar:.2f}/{self.major_league_swar_cap:.1f}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def minor_league_swar_status(self) -> str:
|
def minor_league_swar_status(self) -> str:
|
||||||
@ -450,6 +454,15 @@ class TransactionBuilder:
|
|||||||
is_legal = False
|
is_legal = False
|
||||||
errors.append("Cannot have negative players on Minor League roster")
|
errors.append("Cannot have negative players on Minor League roster")
|
||||||
|
|
||||||
|
# Validate sWAR cap
|
||||||
|
# Use team-specific cap if set, otherwise fall back to config default
|
||||||
|
team_swar_cap = self.team.salary_cap if self.team.salary_cap is not None else config.swar_cap_limit
|
||||||
|
if projected_ml_swar > team_swar_cap:
|
||||||
|
is_legal = False
|
||||||
|
errors.append(f"Major League sWAR would be {projected_ml_swar:.2f} (cap: {team_swar_cap:.1f})")
|
||||||
|
over_cap = projected_ml_swar - team_swar_cap
|
||||||
|
suggestions.append(f"Remove {over_cap:.2f} sWAR from ML roster to be under cap")
|
||||||
|
|
||||||
# Add suggestions for empty transaction
|
# Add suggestions for empty transaction
|
||||||
if not self.moves:
|
if not self.moves:
|
||||||
suggestions.append("Add player moves to build your transaction")
|
suggestions.append("Add player moves to build your transaction")
|
||||||
@ -465,6 +478,7 @@ class TransactionBuilder:
|
|||||||
minor_league_limit=mil_limit,
|
minor_league_limit=mil_limit,
|
||||||
major_league_swar=projected_ml_swar,
|
major_league_swar=projected_ml_swar,
|
||||||
minor_league_swar=projected_mil_swar,
|
minor_league_swar=projected_mil_swar,
|
||||||
|
major_league_swar_cap=team_swar_cap,
|
||||||
pre_existing_ml_swar_change=pre_existing_ml_swar_change,
|
pre_existing_ml_swar_change=pre_existing_ml_swar_change,
|
||||||
pre_existing_mil_swar_change=pre_existing_mil_swar_change,
|
pre_existing_mil_swar_change=pre_existing_mil_swar_change,
|
||||||
pre_existing_transaction_count=pre_existing_count
|
pre_existing_transaction_count=pre_existing_count
|
||||||
|
|||||||
@ -33,7 +33,8 @@ class TestTransactionBuilder:
|
|||||||
abbrev='WV',
|
abbrev='WV',
|
||||||
sname='Black Bears',
|
sname='Black Bears',
|
||||||
lname='West Virginia Black Bears',
|
lname='West Virginia Black Bears',
|
||||||
season=12
|
season=12,
|
||||||
|
salary_cap=50.0 # Set high cap to avoid sWAR validation failures in roster tests
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -411,6 +412,82 @@ class TestTransactionBuilder:
|
|||||||
assert len(validation.suggestions) == 1
|
assert len(validation.suggestions) == 1
|
||||||
assert "Add player moves" in validation.suggestions[0]
|
assert "Add player moves" in validation.suggestions[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_transaction_over_swar_cap(self, mock_roster):
|
||||||
|
"""Test validation fails when sWAR exceeds team cap."""
|
||||||
|
# Create a team with a low salary cap (30.0)
|
||||||
|
low_cap_team = Team(
|
||||||
|
id=499,
|
||||||
|
abbrev='WV',
|
||||||
|
sname='Black Bears',
|
||||||
|
lname='West Virginia Black Bears',
|
||||||
|
season=12,
|
||||||
|
salary_cap=30.0 # Low cap - roster has 24 * 1.5 = 36.0 sWAR
|
||||||
|
)
|
||||||
|
builder = TransactionBuilder(low_cap_team, user_id=12345)
|
||||||
|
|
||||||
|
with patch.object(builder, '_current_roster', mock_roster):
|
||||||
|
with patch.object(builder, '_roster_loaded', True):
|
||||||
|
validation = await builder.validate_transaction()
|
||||||
|
|
||||||
|
# Should fail due to sWAR cap (36.0 > 30.0)
|
||||||
|
assert validation.is_legal is False
|
||||||
|
assert validation.major_league_swar == 36.0 # 24 players * 1.5 wara
|
||||||
|
assert validation.major_league_swar_cap == 30.0
|
||||||
|
assert any("sWAR would be 36.00 (cap: 30.0)" in error for error in validation.errors)
|
||||||
|
assert any("Remove 6.00 sWAR" in suggestion for suggestion in validation.suggestions)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_transaction_under_swar_cap(self, mock_team, mock_roster):
|
||||||
|
"""Test validation passes when sWAR is under team cap."""
|
||||||
|
# mock_team has salary_cap=50.0, roster has 36.0 sWAR
|
||||||
|
builder = TransactionBuilder(mock_team, user_id=12345)
|
||||||
|
|
||||||
|
with patch.object(builder, '_current_roster', mock_roster):
|
||||||
|
with patch.object(builder, '_roster_loaded', True):
|
||||||
|
validation = await builder.validate_transaction()
|
||||||
|
|
||||||
|
# Should pass - 36.0 < 50.0
|
||||||
|
assert validation.is_legal is True
|
||||||
|
assert validation.major_league_swar == 36.0
|
||||||
|
assert validation.major_league_swar_cap == 50.0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_swar_status_display_over_cap(self):
|
||||||
|
"""Test sWAR status display shows correct format when over cap."""
|
||||||
|
validation = RosterValidationResult(
|
||||||
|
is_legal=False,
|
||||||
|
major_league_count=26,
|
||||||
|
minor_league_count=6,
|
||||||
|
warnings=[],
|
||||||
|
errors=[],
|
||||||
|
suggestions=[],
|
||||||
|
major_league_swar=35.5,
|
||||||
|
minor_league_swar=3.0,
|
||||||
|
major_league_swar_cap=33.5
|
||||||
|
)
|
||||||
|
assert "❌" in validation.major_league_swar_status
|
||||||
|
assert "35.50/33.5" in validation.major_league_swar_status
|
||||||
|
assert "Over cap!" in validation.major_league_swar_status
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_swar_status_display_under_cap(self):
|
||||||
|
"""Test sWAR status display shows correct format when under cap."""
|
||||||
|
validation = RosterValidationResult(
|
||||||
|
is_legal=True,
|
||||||
|
major_league_count=26,
|
||||||
|
minor_league_count=6,
|
||||||
|
warnings=[],
|
||||||
|
errors=[],
|
||||||
|
suggestions=[],
|
||||||
|
major_league_swar=31.0,
|
||||||
|
minor_league_swar=3.0,
|
||||||
|
major_league_swar_cap=33.5
|
||||||
|
)
|
||||||
|
assert "✅" in validation.major_league_swar_status
|
||||||
|
assert "31.00/33.5" in validation.major_league_swar_status
|
||||||
|
assert "Over cap!" not in validation.major_league_swar_status
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_submit_transaction_empty(self, builder):
|
async def test_submit_transaction_empty(self, builder):
|
||||||
"""Test submitting empty transaction fails."""
|
"""Test submitting empty transaction fails."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user