Add salary cap helper functions and unit tests

- Add DEFAULT_SALARY_CAP (32.0) and SALARY_CAP_TOLERANCE (0.001) constants
- Add get_team_salary_cap() for retrieving team cap with fallback
- Add exceeds_salary_cap() for centralized cap validation
- Add 17 unit tests covering all edge cases
- Update refactor plan marking P1 tasks complete

These helpers will be used by P2 tasks to replace hardcoded 32.0/32.001
values in draft.py and transactions.py

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-12-09 17:09:25 -06:00
parent 1aaf4ccb50
commit cd8cf0aee8
3 changed files with 257 additions and 9 deletions

View File

@ -26,6 +26,10 @@ PD_SEASON = 9
FA_LOCK_WEEK = 14
SBA_COLOR = 'a6ce39'
# Salary cap constants
DEFAULT_SALARY_CAP = 32.0
SALARY_CAP_TOLERANCE = 0.001 # Small tolerance for floating point comparisons
SBA_ROSTER_KEY = '1bt7LLJe6h7axkhDVlxJ4f319l8QmFB0zQH-pjM0c8a8'
SBA_STATS_KEY = '1fnqx2uxC7DT5aTnx4EkXh83crwrL0W6eJefoC1d4KH4'
SBA_STANDINGS_KEY = '1cXZcPY08RvqV_GeLvZ7PY5-0CyM-AijpJxsaFisZjBc'
@ -1206,3 +1210,39 @@ async def send_owner_notification(message: str):
await session.post(webhook_url, json=payload)
except Exception as webhook_error:
logger.error(f'Failed to send owner notification: {webhook_error}')
def get_team_salary_cap(team: dict) -> float:
"""
Get the salary cap for a team, falling back to the default if not set.
Args:
team: Team dictionary containing team data. May have 'salary_cap' field.
Returns:
float: The team's salary cap, or DEFAULT_SALARY_CAP (32.0) if not set.
Why: Teams may have custom salary caps (e.g., for expansion teams or penalties).
This centralizes the fallback logic so all cap checks use the same source of truth.
"""
if team and team.get('salary_cap') is not None:
return team['salary_cap']
return DEFAULT_SALARY_CAP
def exceeds_salary_cap(wara: float, team: dict) -> bool:
"""
Check if a WAR total exceeds the team's salary cap.
Args:
wara: The total WAR value to check
team: Team dictionary containing team data
Returns:
bool: True if wara exceeds the team's salary cap (with tolerance)
Why: Centralizes the salary cap comparison logic with proper floating point
tolerance handling. All cap validation should use this function.
"""
cap = get_team_salary_cap(team)
return wara > (cap + SALARY_CAP_TOLERANCE)

View File

@ -8,11 +8,11 @@
"name": "Add salary cap helper function",
"description": "Create a helper function in helpers.py that retrieves the salary cap for a team, with fallback to default 32.0 for backwards compatibility",
"files": ["helpers.py"],
"lines": null,
"lines": [1215, 1248],
"priority": 1,
"completed": false,
"completed": true,
"tested": false,
"notes": "This should be done first as other tasks will depend on it"
"notes": "Added get_team_salary_cap() and exceeds_salary_cap() functions"
},
{
"id": "SWAR-002",
@ -74,11 +74,11 @@
"name": "Add default salary cap constant",
"description": "Define DEFAULT_SALARY_CAP = 32.0 constant in helpers.py for use as fallback when team.salary_cap is None",
"files": ["helpers.py"],
"lines": null,
"lines": [30, 31],
"priority": 1,
"completed": false,
"completed": true,
"tested": false,
"notes": "Centralized constant for maintainability"
"notes": "Added DEFAULT_SALARY_CAP and SALARY_CAP_TOLERANCE constants"
},
{
"id": "SWAR-008",
@ -87,9 +87,9 @@
"files": ["api_calls/team.py"],
"lines": [26],
"priority": 1,
"completed": false,
"completed": true,
"tested": false,
"notes": "Field already added, need to verify API integration"
"notes": "Field exists: salary_cap: Optional[float] = None"
}
],
"completion_checklist": [

208
tests/test_salary_cap.py Normal file
View File

@ -0,0 +1,208 @@
"""
Unit tests for salary cap helper functions in helpers.py.
These tests verify:
1. get_team_salary_cap() returns correct cap values with fallback behavior
2. exceeds_salary_cap() correctly identifies when WAR exceeds team cap
3. Edge cases around None values and floating point tolerance
Why these tests matter:
- Salary cap validation is critical for league integrity during trades/drafts
- The helper functions centralize logic previously scattered across cogs
- Proper fallback behavior ensures backwards compatibility
"""
import pytest
from helpers import (
DEFAULT_SALARY_CAP,
SALARY_CAP_TOLERANCE,
get_team_salary_cap,
exceeds_salary_cap
)
class TestGetTeamSalaryCap:
"""Tests for get_team_salary_cap() function."""
def test_returns_team_salary_cap_when_set(self):
"""
When a team has a custom salary_cap value set, return that value.
Why: Some teams may have different caps (expansion teams, penalties, etc.)
"""
team = {'abbrev': 'TEST', 'salary_cap': 35.0}
result = get_team_salary_cap(team)
assert result == 35.0
def test_returns_default_when_salary_cap_is_none(self):
"""
When team.salary_cap is None, return the default cap (32.0).
Why: Most teams use the standard cap; None indicates no custom value.
"""
team = {'abbrev': 'TEST', 'salary_cap': None}
result = get_team_salary_cap(team)
assert result == DEFAULT_SALARY_CAP
assert result == 32.0
def test_returns_default_when_salary_cap_key_missing(self):
"""
When the salary_cap key doesn't exist in team dict, return default.
Why: Backwards compatibility with older team data structures.
"""
team = {'abbrev': 'TEST', 'sname': 'Test Team'}
result = get_team_salary_cap(team)
assert result == DEFAULT_SALARY_CAP
def test_returns_default_when_team_is_none(self):
"""
When team is None, return the default cap.
Why: Defensive programming - callers may pass None in edge cases.
"""
result = get_team_salary_cap(None)
assert result == DEFAULT_SALARY_CAP
def test_returns_default_when_team_is_empty_dict(self):
"""
When team is an empty dict, return the default cap.
Why: Edge case handling for malformed team data.
"""
result = get_team_salary_cap({})
assert result == DEFAULT_SALARY_CAP
def test_respects_zero_salary_cap(self):
"""
When salary_cap is explicitly 0, return 0 (not default).
Why: Zero is a valid value (e.g., suspended team), distinct from None.
"""
team = {'abbrev': 'BANNED', 'salary_cap': 0.0}
result = get_team_salary_cap(team)
assert result == 0.0
def test_handles_integer_salary_cap(self):
"""
When salary_cap is an integer, return it as-is.
Why: API may return int instead of float; function should handle both.
"""
team = {'abbrev': 'TEST', 'salary_cap': 30}
result = get_team_salary_cap(team)
assert result == 30
class TestExceedsSalaryCap:
"""Tests for exceeds_salary_cap() function."""
def test_returns_false_when_under_cap(self):
"""
WAR of 30.0 should not exceed default cap of 32.0.
Why: Normal case - team is under cap and should pass validation.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
result = exceeds_salary_cap(30.0, team)
assert result is False
def test_returns_false_when_exactly_at_cap(self):
"""
WAR of exactly 32.0 should not exceed cap (within tolerance).
Why: Teams should be allowed to be exactly at cap limit.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
result = exceeds_salary_cap(32.0, team)
assert result is False
def test_returns_false_within_tolerance(self):
"""
WAR slightly above cap but within tolerance should not exceed.
Why: Floating point math may produce values like 32.0000001;
tolerance prevents false positives from rounding errors.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
# 32.0005 is within 0.001 tolerance of 32.0
result = exceeds_salary_cap(32.0005, team)
assert result is False
def test_returns_true_when_over_cap(self):
"""
WAR of 33.0 clearly exceeds cap of 32.0.
Why: Core validation - must reject teams over cap.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
result = exceeds_salary_cap(33.0, team)
assert result is True
def test_returns_true_just_over_tolerance(self):
"""
WAR just beyond tolerance should exceed cap.
Why: Tolerance has a boundary; values beyond it must fail.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
# 32.002 is beyond 0.001 tolerance
result = exceeds_salary_cap(32.002, team)
assert result is True
def test_uses_team_custom_cap(self):
"""
Should use team's custom cap, not default.
Why: Teams with higher/lower caps must be validated correctly.
"""
team = {'abbrev': 'EXPANSION', 'salary_cap': 28.0}
# 30.0 is under default 32.0 but over custom 28.0
result = exceeds_salary_cap(30.0, team)
assert result is True
def test_uses_default_cap_when_team_cap_none(self):
"""
When team has no custom cap, use default for comparison.
Why: Backwards compatibility - existing teams without salary_cap field.
"""
team = {'abbrev': 'TEST', 'salary_cap': None}
result = exceeds_salary_cap(33.0, team)
assert result is True
result = exceeds_salary_cap(31.0, team)
assert result is False
def test_handles_none_team(self):
"""
When team is None, use default cap for comparison.
Why: Defensive programming for edge cases.
"""
result = exceeds_salary_cap(33.0, None)
assert result is True
result = exceeds_salary_cap(31.0, None)
assert result is False
class TestConstants:
"""Tests for salary cap constants."""
def test_default_salary_cap_value(self):
"""
DEFAULT_SALARY_CAP should be 32.0 (league standard).
Why: Ensures constant wasn't accidentally changed.
"""
assert DEFAULT_SALARY_CAP == 32.0
def test_tolerance_value(self):
"""
SALARY_CAP_TOLERANCE should be 0.001.
Why: Tolerance must be small enough to catch real violations
but large enough to handle floating point imprecision.
"""
assert SALARY_CAP_TOLERANCE == 0.001