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:
parent
1aaf4ccb50
commit
cd8cf0aee8
40
helpers.py
40
helpers.py
@ -26,6 +26,10 @@ PD_SEASON = 9
|
|||||||
FA_LOCK_WEEK = 14
|
FA_LOCK_WEEK = 14
|
||||||
SBA_COLOR = 'a6ce39'
|
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_ROSTER_KEY = '1bt7LLJe6h7axkhDVlxJ4f319l8QmFB0zQH-pjM0c8a8'
|
||||||
SBA_STATS_KEY = '1fnqx2uxC7DT5aTnx4EkXh83crwrL0W6eJefoC1d4KH4'
|
SBA_STATS_KEY = '1fnqx2uxC7DT5aTnx4EkXh83crwrL0W6eJefoC1d4KH4'
|
||||||
SBA_STANDINGS_KEY = '1cXZcPY08RvqV_GeLvZ7PY5-0CyM-AijpJxsaFisZjBc'
|
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)
|
await session.post(webhook_url, json=payload)
|
||||||
except Exception as webhook_error:
|
except Exception as webhook_error:
|
||||||
logger.error(f'Failed to send owner notification: {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)
|
||||||
|
|||||||
@ -8,11 +8,11 @@
|
|||||||
"name": "Add salary cap helper function",
|
"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",
|
"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"],
|
"files": ["helpers.py"],
|
||||||
"lines": null,
|
"lines": [1215, 1248],
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"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",
|
"id": "SWAR-002",
|
||||||
@ -74,11 +74,11 @@
|
|||||||
"name": "Add default salary cap constant",
|
"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",
|
"description": "Define DEFAULT_SALARY_CAP = 32.0 constant in helpers.py for use as fallback when team.salary_cap is None",
|
||||||
"files": ["helpers.py"],
|
"files": ["helpers.py"],
|
||||||
"lines": null,
|
"lines": [30, 31],
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"notes": "Centralized constant for maintainability"
|
"notes": "Added DEFAULT_SALARY_CAP and SALARY_CAP_TOLERANCE constants"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "SWAR-008",
|
"id": "SWAR-008",
|
||||||
@ -87,9 +87,9 @@
|
|||||||
"files": ["api_calls/team.py"],
|
"files": ["api_calls/team.py"],
|
||||||
"lines": [26],
|
"lines": [26],
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"notes": "Field already added, need to verify API integration"
|
"notes": "Field exists: salary_cap: Optional[float] = None"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"completion_checklist": [
|
"completion_checklist": [
|
||||||
|
|||||||
208
tests/test_salary_cap.py
Normal file
208
tests/test_salary_cap.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user