feat: enforce trade deadline in /trade commands #121

Merged
cal merged 1 commits from feature/trade-deadline-enforcement into main 2026-03-30 21:46:19 +00:00
5 changed files with 501 additions and 261 deletions

View File

@ -26,6 +26,7 @@ from services.trade_builder import (
clear_trade_builder,
clear_trade_builder_by_team,
)
from services.league_service import league_service
from services.player_service import player_service
from services.team_service import team_service
from models.team import RosterType
@ -130,6 +131,22 @@ class TradeCommands(commands.Cog):
)
return
# Check trade deadline
current = await league_service.get_current_state()
if not current:
await interaction.followup.send(
"❌ Could not retrieve league state. Please try again later.",
ephemeral=True,
)
return
if current.is_past_trade_deadline:
await interaction.followup.send(
f"❌ **The trade deadline has passed.** The deadline was Week {current.trade_deadline} "
f"and we are currently in Week {current.week}. No new trades can be initiated.",
ephemeral=True,
)
return
# Clear any existing trade and create new one
clear_trade_builder(interaction.user.id)
trade_builder = get_trade_builder(interaction.user.id, user_team)

View File

@ -3,6 +3,7 @@ Current league state model
Represents the current state of the league including week, season, and settings.
"""
from pydantic import Field, field_validator
from models.base import SBABaseModel
@ -10,38 +11,45 @@ from models.base import SBABaseModel
class Current(SBABaseModel):
"""Model representing current league state and settings."""
week: int = Field(69, description="Current week number")
season: int = Field(69, description="Current season number")
freeze: bool = Field(True, description="Whether league is frozen")
bet_week: str = Field('sheets', description="Betting week identifier")
bet_week: str = Field("sheets", description="Betting week identifier")
trade_deadline: int = Field(1, description="Trade deadline week")
pick_trade_start: int = Field(69, description="Draft pick trading start week")
pick_trade_end: int = Field(420, description="Draft pick trading end week")
playoffs_begin: int = Field(420, description="Week when playoffs begin")
@field_validator("bet_week", mode="before")
@classmethod
def cast_bet_week_to_string(cls, v):
"""Ensure bet_week is always a string."""
return str(v) if v is not None else 'sheets'
return str(v) if v is not None else "sheets"
@property
def is_offseason(self) -> bool:
"""Check if league is currently in offseason."""
return self.week > 18
@property
def is_playoffs(self) -> bool:
"""Check if league is currently in playoffs."""
return self.week >= self.playoffs_begin
@property
def can_trade_picks(self) -> bool:
"""Check if draft pick trading is currently allowed."""
return self.pick_trade_start <= self.week <= self.pick_trade_end
@property
def ever_trade_picks(self) -> bool:
"""Check if draft pick trading is allowed this season at all"""
return self.pick_trade_start <= self.playoffs_begin + 4
return self.pick_trade_start <= self.playoffs_begin + 4
@property
def is_past_trade_deadline(self) -> bool:
"""Check if the trade deadline has passed."""
if self.is_offseason:
return False
return self.week > self.trade_deadline

View File

@ -0,0 +1,143 @@
"""
Tests for trade deadline enforcement in /trade commands.
Validates that trades are blocked after the trade deadline and allowed during/before it.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from tests.factories import CurrentFactory, TeamFactory
class TestTradeInitiateDeadlineGuard:
"""Test trade deadline enforcement in /trade initiate command."""
@pytest.fixture
def mock_interaction(self):
"""Create mock Discord interaction with deferred response."""
interaction = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 258104532423147520
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
interaction.guild = MagicMock()
interaction.guild.id = 669356687294988350
return interaction
@pytest.mark.asyncio
async def test_trade_initiate_blocked_past_deadline(self, mock_interaction):
"""After the trade deadline, /trade initiate should return a friendly error."""
user_team = TeamFactory.west_virginia()
other_team = TeamFactory.new_york()
past_deadline = CurrentFactory.create(week=15, trade_deadline=14)
with (
patch(
"commands.transactions.trade.validate_user_has_team",
new_callable=AsyncMock,
return_value=user_team,
),
patch(
"commands.transactions.trade.get_team_by_abbrev_with_validation",
new_callable=AsyncMock,
return_value=other_team,
),
patch("commands.transactions.trade.league_service") as mock_league,
):
mock_league.get_current_state = AsyncMock(return_value=past_deadline)
from commands.transactions.trade import TradeCommands
bot = MagicMock()
cog = TradeCommands(bot)
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
mock_interaction.followup.send.assert_called_once()
call_kwargs = mock_interaction.followup.send.call_args
msg = (
call_kwargs[0][0]
if call_kwargs[0]
else call_kwargs[1].get("content", "")
)
assert "trade deadline has passed" in msg.lower()
@pytest.mark.asyncio
async def test_trade_initiate_allowed_at_deadline_week(self, mock_interaction):
"""During the deadline week itself, /trade initiate should proceed."""
user_team = TeamFactory.west_virginia()
other_team = TeamFactory.new_york()
at_deadline = CurrentFactory.create(week=14, trade_deadline=14)
with (
patch(
"commands.transactions.trade.validate_user_has_team",
new_callable=AsyncMock,
return_value=user_team,
),
patch(
"commands.transactions.trade.get_team_by_abbrev_with_validation",
new_callable=AsyncMock,
return_value=other_team,
),
patch("commands.transactions.trade.league_service") as mock_league,
patch("commands.transactions.trade.clear_trade_builder") as mock_clear,
patch("commands.transactions.trade.get_trade_builder") as mock_get_builder,
patch(
"commands.transactions.trade.create_trade_embed",
new_callable=AsyncMock,
return_value=MagicMock(),
),
):
mock_league.get_current_state = AsyncMock(return_value=at_deadline)
mock_builder = MagicMock()
mock_builder.add_team = AsyncMock(return_value=(True, None))
mock_builder.trade_id = "test-123"
mock_get_builder.return_value = mock_builder
from commands.transactions.trade import TradeCommands
bot = MagicMock()
cog = TradeCommands(bot)
cog.channel_manager = MagicMock()
cog.channel_manager.create_trade_channel = AsyncMock(return_value=None)
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
# Should have proceeded past deadline check to clear/create trade
mock_clear.assert_called_once()
@pytest.mark.asyncio
async def test_trade_initiate_blocked_when_current_none(self, mock_interaction):
"""When league state can't be fetched, /trade initiate should fail closed."""
user_team = TeamFactory.west_virginia()
other_team = TeamFactory.new_york()
with (
patch(
"commands.transactions.trade.validate_user_has_team",
new_callable=AsyncMock,
return_value=user_team,
),
patch(
"commands.transactions.trade.get_team_by_abbrev_with_validation",
new_callable=AsyncMock,
return_value=other_team,
),
patch("commands.transactions.trade.league_service") as mock_league,
):
mock_league.get_current_state = AsyncMock(return_value=None)
from commands.transactions.trade import TradeCommands
bot = MagicMock()
cog = TradeCommands(bot)
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
mock_interaction.followup.send.assert_called_once()
call_kwargs = mock_interaction.followup.send.call_args
msg = (
call_kwargs[0][0]
if call_kwargs[0]
else call_kwargs[1].get("content", "")
)
assert "could not retrieve league state" in msg.lower()

View File

@ -3,6 +3,7 @@ Tests for SBA data models
Validates model creation, validation, and business logic.
"""
import pytest
from models import Team, Player, Current, DraftPick, DraftData, DraftList
@ -10,94 +11,102 @@ from models import Team, Player, Current, DraftPick, DraftData, DraftList
class TestSBABaseModel:
"""Test base model functionality."""
def test_model_creation_with_api_data(self):
"""Test creating models from API data."""
team_data = {
'id': 1,
'abbrev': 'NYY',
'sname': 'Yankees',
'lname': 'New York Yankees',
'season': 12
"id": 1,
"abbrev": "NYY",
"sname": "Yankees",
"lname": "New York Yankees",
"season": 12,
}
team = Team.from_api_data(team_data)
assert team.id == 1
assert team.abbrev == 'NYY'
assert team.lname == 'New York Yankees'
assert team.abbrev == "NYY"
assert team.lname == "New York Yankees"
def test_to_dict_functionality(self):
"""Test model to dictionary conversion."""
team = Team(id=1, abbrev='LAA', sname='Angels', lname='Los Angeles Angels', season=12)
team = Team(
id=1, abbrev="LAA", sname="Angels", lname="Los Angeles Angels", season=12
)
team_dict = team.to_dict()
assert 'abbrev' in team_dict
assert team_dict['abbrev'] == 'LAA'
assert team_dict['lname'] == 'Los Angeles Angels'
assert "abbrev" in team_dict
assert team_dict["abbrev"] == "LAA"
assert team_dict["lname"] == "Los Angeles Angels"
def test_model_repr(self):
"""Test model string representation."""
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
team = Team(
id=2, abbrev="BOS", sname="Red Sox", lname="Boston Red Sox", season=12
)
repr_str = repr(team)
assert 'Team(' in repr_str
assert 'abbrev=BOS' in repr_str
assert "Team(" in repr_str
assert "abbrev=BOS" in repr_str
class TestTeamModel:
"""Test Team model functionality."""
def test_team_creation_minimal(self):
"""Test team creation with minimal required fields."""
team = Team(
id=4,
abbrev='HOU',
sname='Astros',
lname='Houston Astros',
season=12
id=4, abbrev="HOU", sname="Astros", lname="Houston Astros", season=12
)
assert team.abbrev == 'HOU'
assert team.sname == 'Astros'
assert team.lname == 'Houston Astros'
assert team.abbrev == "HOU"
assert team.sname == "Astros"
assert team.lname == "Houston Astros"
assert team.season == 12
def test_team_creation_with_optional_fields(self):
"""Test team creation with optional fields."""
team = Team(
id=5,
abbrev='SF',
sname='Giants',
lname='San Francisco Giants',
abbrev="SF",
sname="Giants",
lname="San Francisco Giants",
season=12,
gmid=100,
division_id=1,
stadium='Oracle Park',
color='FF8C00'
stadium="Oracle Park",
color="FF8C00",
)
assert team.gmid == 100
assert team.division_id == 1
assert team.stadium == 'Oracle Park'
assert team.color == 'FF8C00'
assert team.stadium == "Oracle Park"
assert team.color == "FF8C00"
def test_team_str_representation(self):
"""Test team string representation."""
team = Team(id=3, abbrev='SD', sname='Padres', lname='San Diego Padres', season=12)
assert str(team) == 'SD - San Diego Padres'
team = Team(
id=3, abbrev="SD", sname="Padres", lname="San Diego Padres", season=12
)
assert str(team) == "SD - San Diego Padres"
def test_team_roster_type_major_league(self):
"""Test roster type detection for Major League teams."""
from models.team import RosterType
# 3 chars or less → Major League
team = Team(id=1, abbrev='NYY', sname='Yankees', lname='New York Yankees', season=12)
team = Team(
id=1, abbrev="NYY", sname="Yankees", lname="New York Yankees", season=12
)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
team = Team(
id=2, abbrev="BOS", sname="Red Sox", lname="Boston Red Sox", season=12
)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
# Even "BHM" (ends in M) should be Major League
team = Team(id=3, abbrev='BHM', sname='Iron', lname='Birmingham Iron', season=12)
team = Team(
id=3, abbrev="BHM", sname="Iron", lname="Birmingham Iron", season=12
)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
def test_team_roster_type_minor_league(self):
@ -105,14 +114,28 @@ class TestTeamModel:
from models.team import RosterType
# Standard Minor League: [Team] + "MIL"
team = Team(id=4, abbrev='NYYMIL', sname='RailRiders', lname='Staten Island RailRiders', season=12)
team = Team(
id=4,
abbrev="NYYMIL",
sname="RailRiders",
lname="Staten Island RailRiders",
season=12,
)
assert team.roster_type() == RosterType.MINOR_LEAGUE
team = Team(id=5, abbrev='PORMIL', sname='Portland MiL', lname='Portland Minor League', season=12)
team = Team(
id=5,
abbrev="PORMIL",
sname="Portland MiL",
lname="Portland Minor League",
season=12,
)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# Case insensitive
team = Team(id=6, abbrev='LAAmil', sname='Bees', lname='Salt Lake Bees', season=12)
team = Team(
id=6, abbrev="LAAmil", sname="Bees", lname="Salt Lake Bees", season=12
)
assert team.roster_type() == RosterType.MINOR_LEAGUE
def test_team_roster_type_injured_list(self):
@ -120,14 +143,32 @@ class TestTeamModel:
from models.team import RosterType
# Standard Injured List: [Team] + "IL"
team = Team(id=7, abbrev='NYYIL', sname='Yankees IL', lname='New York Yankees IL', season=12)
team = Team(
id=7,
abbrev="NYYIL",
sname="Yankees IL",
lname="New York Yankees IL",
season=12,
)
assert team.roster_type() == RosterType.INJURED_LIST
team = Team(id=8, abbrev='PORIL', sname='Loggers IL', lname='Portland Loggers IL', season=12)
team = Team(
id=8,
abbrev="PORIL",
sname="Loggers IL",
lname="Portland Loggers IL",
season=12,
)
assert team.roster_type() == RosterType.INJURED_LIST
# Case insensitive
team = Team(id=9, abbrev='LAAil', sname='Angels IL', lname='Los Angeles Angels IL', season=12)
team = Team(
id=9,
abbrev="LAAil",
sname="Angels IL",
lname="Los Angeles Angels IL",
season=12,
)
assert team.roster_type() == RosterType.INJURED_LIST
def test_team_roster_type_edge_case_bhmil(self):
@ -143,16 +184,30 @@ class TestTeamModel:
from models.team import RosterType
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
team = Team(id=10, abbrev='BHMIL', sname='Iron IL', lname='Birmingham Iron IL', season=12)
team = Team(
id=10,
abbrev="BHMIL",
sname="Iron IL",
lname="Birmingham Iron IL",
season=12,
)
assert team.roster_type() == RosterType.INJURED_LIST
# Compare with a real Minor League team that has "Island" in name
# "NYYMIL" = "NYY" + "MIL", even though sname has "Island" → MINOR_LEAGUE
team = Team(id=11, abbrev='NYYMIL', sname='Staten Island RailRiders', lname='Staten Island RailRiders', season=12)
team = Team(
id=11,
abbrev="NYYMIL",
sname="Staten Island RailRiders",
lname="Staten Island RailRiders",
season=12,
)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# Another IL edge case with sname containing "IL" at word boundary
team = Team(id=12, abbrev='WVMIL', sname='WV IL', lname='West Virginia IL', season=12)
team = Team(
id=12, abbrev="WVMIL", sname="WV IL", lname="West Virginia IL", season=12
)
assert team.roster_type() == RosterType.INJURED_LIST
def test_team_roster_type_sname_disambiguation(self):
@ -160,221 +215,231 @@ class TestTeamModel:
from models.team import RosterType
# MiL team - sname does NOT have "IL" as a word
team = Team(id=13, abbrev='WVMIL', sname='Miners', lname='West Virginia Miners', season=12)
team = Team(
id=13,
abbrev="WVMIL",
sname="Miners",
lname="West Virginia Miners",
season=12,
)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# IL team - sname has "IL" at word boundary
team = Team(id=14, abbrev='WVMIL', sname='Miners IL', lname='West Virginia Miners IL', season=12)
team = Team(
id=14,
abbrev="WVMIL",
sname="Miners IL",
lname="West Virginia Miners IL",
season=12,
)
assert team.roster_type() == RosterType.INJURED_LIST
# MiL team - sname has "IL" but only in "Island" (substring, not word boundary)
team = Team(id=15, abbrev='CHIMIL', sname='Island Hoppers', lname='Chicago Island Hoppers', season=12)
team = Team(
id=15,
abbrev="CHIMIL",
sname="Island Hoppers",
lname="Chicago Island Hoppers",
season=12,
)
assert team.roster_type() == RosterType.MINOR_LEAGUE
class TestPlayerModel:
"""Test Player model functionality."""
def test_player_creation(self):
"""Test player creation with required fields."""
player = Player(
id=101,
name='Mike Trout',
name="Mike Trout",
wara=8.5,
season=12,
team_id=1,
image='trout.jpg',
pos_1='CF'
image="trout.jpg",
pos_1="CF",
)
assert player.name == 'Mike Trout'
assert player.name == "Mike Trout"
assert player.wara == 8.5
assert player.team_id == 1
assert player.pos_1 == 'CF'
assert player.pos_1 == "CF"
def test_player_positions_property(self):
"""Test player positions property."""
player = Player(
id=102,
name='Shohei Ohtani',
name="Shohei Ohtani",
wara=9.0,
season=12,
team_id=1,
image='ohtani.jpg',
pos_1='SP',
pos_2='DH',
pos_3='RF'
image="ohtani.jpg",
pos_1="SP",
pos_2="DH",
pos_3="RF",
)
positions = player.positions
assert len(positions) == 3
assert 'SP' in positions
assert 'DH' in positions
assert 'RF' in positions
assert "SP" in positions
assert "DH" in positions
assert "RF" in positions
def test_player_primary_position(self):
"""Test primary position property."""
player = Player(
id=103,
name='Mookie Betts',
name="Mookie Betts",
wara=7.2,
season=12,
team_id=1,
image='betts.jpg',
pos_1='RF',
pos_2='2B'
image="betts.jpg",
pos_1="RF",
pos_2="2B",
)
assert player.primary_position == 'RF'
assert player.primary_position == "RF"
def test_player_is_pitcher(self):
"""Test is_pitcher property."""
pitcher = Player(
id=104,
name='Gerrit Cole',
name="Gerrit Cole",
wara=6.8,
season=12,
team_id=1,
image='cole.jpg',
pos_1='SP'
image="cole.jpg",
pos_1="SP",
)
position_player = Player(
id=105,
name='Aaron Judge',
name="Aaron Judge",
wara=8.1,
season=12,
team_id=1,
image='judge.jpg',
pos_1='RF'
image="judge.jpg",
pos_1="RF",
)
assert pitcher.is_pitcher is True
assert position_player.is_pitcher is False
def test_player_str_representation(self):
"""Test player string representation."""
player = Player(
id=106,
name='Ronald Acuna Jr.',
name="Ronald Acuna Jr.",
wara=8.8,
season=12,
team_id=1,
image='acuna.jpg',
pos_1='OF'
image="acuna.jpg",
pos_1="OF",
)
assert str(player) == 'Ronald Acuna Jr. (OF)'
assert str(player) == "Ronald Acuna Jr. (OF)"
class TestCurrentModel:
"""Test Current league state model."""
def test_current_default_values(self):
"""Test current model with default values."""
current = Current()
assert current.week == 69
assert current.season == 69
assert current.freeze is True
assert current.bet_week == 'sheets'
assert current.bet_week == "sheets"
def test_current_with_custom_values(self):
"""Test current model with custom values."""
current = Current(
week=15,
season=12,
freeze=False,
trade_deadline=14,
playoffs_begin=19
week=15, season=12, freeze=False, trade_deadline=14, playoffs_begin=19
)
assert current.week == 15
assert current.season == 12
assert current.freeze is False
def test_current_properties(self):
"""Test current model properties."""
# Regular season
current = Current(week=10, playoffs_begin=19)
assert current.is_offseason is False
assert current.is_playoffs is False
# Playoffs
current = Current(week=20, playoffs_begin=19)
assert current.is_offseason is True
assert current.is_playoffs is True
# Pick trading
current = Current(week=15, pick_trade_start=10, pick_trade_end=20)
assert current.can_trade_picks is True
def test_is_past_trade_deadline(self):
"""Test trade deadline property — trades allowed during deadline week, blocked after."""
# Before deadline
current = Current(week=10, trade_deadline=14)
assert current.is_past_trade_deadline is False
# At deadline week (still allowed)
current = Current(week=14, trade_deadline=14)
assert current.is_past_trade_deadline is False
# One week past deadline
current = Current(week=15, trade_deadline=14)
assert current.is_past_trade_deadline is True
# Offseason bypasses deadline (week > 18)
current = Current(week=20, trade_deadline=14)
assert current.is_offseason is True
assert current.is_past_trade_deadline is False
class TestDraftPickModel:
"""Test DraftPick model functionality."""
def test_draft_pick_creation(self):
"""Test draft pick creation."""
pick = DraftPick(
season=12,
overall=1,
round=1,
origowner_id=1,
owner_id=1
)
pick = DraftPick(season=12, overall=1, round=1, origowner_id=1, owner_id=1)
assert pick.season == 12
assert pick.overall == 1
assert pick.origowner_id == 1
assert pick.owner_id == 1
def test_draft_pick_properties(self):
"""Test draft pick properties."""
# Not traded, not selected
pick = DraftPick(
season=12,
overall=5,
round=1,
origowner_id=1,
owner_id=1
)
pick = DraftPick(season=12, overall=5, round=1, origowner_id=1, owner_id=1)
assert pick.is_traded is False
assert pick.is_selected is False
# Traded pick
traded_pick = DraftPick(
season=12,
overall=10,
round=1,
origowner_id=1,
owner_id=2
season=12, overall=10, round=1, origowner_id=1, owner_id=2
)
assert traded_pick.is_traded is True
# Selected pick
selected_pick = DraftPick(
season=12,
overall=15,
round=1,
origowner_id=1,
owner_id=1,
player_id=100
season=12, overall=15, round=1, origowner_id=1, owner_id=1, player_id=100
)
assert selected_pick.is_selected is True
class TestDraftDataModel:
"""Test DraftData model functionality."""
def test_draft_data_creation(self):
"""Test draft data creation."""
draft_data = DraftData(
result_channel=123456789,
ping_channel=987654321,
pick_minutes=10
result_channel=123456789, ping_channel=987654321, pick_minutes=10
)
assert draft_data.result_channel == 123456789
@ -384,20 +449,12 @@ class TestDraftDataModel:
def test_draft_data_properties(self):
"""Test draft data properties."""
# Inactive draft
draft_data = DraftData(
result_channel=123,
ping_channel=456,
timer=False
)
draft_data = DraftData(result_channel=123, ping_channel=456, timer=False)
assert draft_data.is_draft_active is False
# Active draft
active_draft = DraftData(
result_channel=123,
ping_channel=456,
timer=True
)
active_draft = DraftData(result_channel=123, ping_channel=456, timer=True)
assert active_draft.is_draft_active is True
@ -409,17 +466,13 @@ class TestDraftListModel:
not just IDs. The API returns these objects populated.
"""
def _create_mock_team(self, team_id: int = 1) -> 'Team':
def _create_mock_team(self, team_id: int = 1) -> "Team":
"""Create a mock team for testing."""
return Team(
id=team_id,
abbrev="TST",
sname="Test",
lname="Test Team",
season=12
id=team_id, abbrev="TST", sname="Test", lname="Test Team", season=12
)
def _create_mock_player(self, player_id: int = 100) -> 'Player':
def _create_mock_player(self, player_id: int = 100) -> "Player":
"""Create a mock player for testing."""
return Player(
id=player_id,
@ -430,7 +483,7 @@ class TestDraftListModel:
team_id=1,
season=12,
wara=2.5,
image="https://example.com/test.jpg"
image="https://example.com/test.jpg",
)
def test_draft_list_creation(self):
@ -438,12 +491,7 @@ class TestDraftListModel:
mock_team = self._create_mock_team(team_id=1)
mock_player = self._create_mock_player(player_id=100)
draft_entry = DraftList(
season=12,
team=mock_team,
rank=1,
player=mock_player
)
draft_entry = DraftList(season=12, team=mock_team, rank=1, player=mock_player)
assert draft_entry.season == 12
assert draft_entry.team_id == 1
@ -456,18 +504,10 @@ class TestDraftListModel:
mock_player_top = self._create_mock_player(player_id=100)
mock_player_lower = self._create_mock_player(player_id=200)
top_pick = DraftList(
season=12,
team=mock_team,
rank=1,
player=mock_player_top
)
top_pick = DraftList(season=12, team=mock_team, rank=1, player=mock_player_top)
lower_pick = DraftList(
season=12,
team=mock_team,
rank=5,
player=mock_player_lower
season=12, team=mock_team, rank=5, player=mock_player_lower
)
assert top_pick.is_top_ranked is True
@ -486,32 +526,32 @@ class TestDraftListModel:
"""
# Simulate API response format - nested objects, NOT flat IDs
api_response = {
'id': 303,
'season': 13,
'rank': 1,
'team': {
'id': 548,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'season': 13
"id": 303,
"season": 13,
"rank": 1,
"team": {
"id": 548,
"abbrev": "WV",
"sname": "Black Bears",
"lname": "West Virginia Black Bears",
"season": 13,
},
'player': {
'id': 12843,
'name': 'George Springer',
'wara': 0.31,
'image': 'https://example.com/springer.png',
'season': 13,
'pos_1': 'CF',
"player": {
"id": 12843,
"name": "George Springer",
"wara": 0.31,
"image": "https://example.com/springer.png",
"season": 13,
"pos_1": "CF",
# Note: NO flat team_id here - it's nested in 'team' below
'team': {
'id': 547, # Free Agent team
'abbrev': 'FA',
'sname': 'Free Agents',
'lname': 'Free Agents',
'season': 13
}
}
"team": {
"id": 547, # Free Agent team
"abbrev": "FA",
"sname": "Free Agents",
"lname": "Free Agents",
"season": 13,
},
},
}
# Create DraftList using from_api_data (what BaseService calls)
@ -522,87 +562,94 @@ class TestDraftListModel:
assert draft_entry.player is not None
# CRITICAL: player.team_id must be extracted from nested team object
assert draft_entry.player.team_id == 547, \
assert draft_entry.player.team_id == 547, (
f"player.team_id should be 547 (FA), got {draft_entry.player.team_id}"
)
# Verify the nested team object is also populated
assert draft_entry.player.team is not None
assert draft_entry.player.team.id == 547
assert draft_entry.player.team.abbrev == 'FA'
assert draft_entry.player.team.abbrev == "FA"
# Verify DraftList's own team data
assert draft_entry.team.id == 548
assert draft_entry.team.abbrev == 'WV'
assert draft_entry.team.abbrev == "WV"
assert draft_entry.team_id == 548 # Property from nested team
class TestModelCoverageExtras:
"""Additional model coverage tests."""
def test_base_model_from_api_data_validation(self):
"""Test from_api_data with various edge cases."""
from models.base import SBABaseModel
# Test with empty data raises ValueError
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
with pytest.raises(
ValueError, match="Cannot create SBABaseModel from empty data"
):
SBABaseModel.from_api_data({})
# Test with None raises ValueError
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
with pytest.raises(
ValueError, match="Cannot create SBABaseModel from empty data"
):
SBABaseModel.from_api_data(None)
def test_player_positions_comprehensive(self):
"""Test player positions property with all position variations."""
player_data = {
'id': 201,
'name': 'Multi-Position Player',
'wara': 3.0,
'season': 12,
'team_id': 5,
'image': 'https://example.com/player.jpg',
'pos_1': 'C',
'pos_2': '1B',
'pos_3': '3B',
'pos_4': None, # Test None handling
'pos_5': 'DH',
'pos_6': 'OF',
'pos_7': None, # Another None
'pos_8': 'SS'
"id": 201,
"name": "Multi-Position Player",
"wara": 3.0,
"season": 12,
"team_id": 5,
"image": "https://example.com/player.jpg",
"pos_1": "C",
"pos_2": "1B",
"pos_3": "3B",
"pos_4": None, # Test None handling
"pos_5": "DH",
"pos_6": "OF",
"pos_7": None, # Another None
"pos_8": "SS",
}
player = Player.from_api_data(player_data)
positions = player.positions
assert 'C' in positions
assert '1B' in positions
assert '3B' in positions
assert 'DH' in positions
assert 'OF' in positions
assert 'SS' in positions
assert "C" in positions
assert "1B" in positions
assert "3B" in positions
assert "DH" in positions
assert "OF" in positions
assert "SS" in positions
assert len(positions) == 6 # Should exclude None values
assert None not in positions
def test_player_is_pitcher_variations(self):
"""Test is_pitcher property with different positions."""
test_cases = [
('SP', True), # Starting pitcher
('RP', True), # Relief pitcher
('P', True), # Generic pitcher
('C', False), # Catcher
('1B', False), # First base
('OF', False), # Outfield
('DH', False), # Designated hitter
("SP", True), # Starting pitcher
("RP", True), # Relief pitcher
("P", True), # Generic pitcher
("C", False), # Catcher
("1B", False), # First base
("OF", False), # Outfield
("DH", False), # Designated hitter
]
for position, expected in test_cases:
player_data = {
'id': 300 + ord(position[0]), # Generate unique IDs based on position
'name': f'Test {position}',
'wara': 2.0,
'season': 12,
'team_id': 5,
'image': 'https://example.com/player.jpg',
'pos_1': position,
"id": 300 + ord(position[0]), # Generate unique IDs based on position
"name": f"Test {position}",
"wara": 2.0,
"season": 12,
"team_id": 5,
"image": "https://example.com/player.jpg",
"pos_1": position,
}
player = Player.from_api_data(player_data)
assert player.is_pitcher == expected, f"Position {position} should return {expected}"
assert player.primary_position == position
assert player.is_pitcher == expected, (
f"Position {position} should return {expected}"
)
assert player.primary_position == position

View File

@ -124,6 +124,22 @@ class TradeEmbedView(discord.ui.View):
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle submit trade button click."""
# Check trade deadline
current = await league_service.get_current_state()
if not current:
await interaction.response.send_message(
"❌ Could not retrieve league state. Please try again later.",
ephemeral=True,
)
return
if current.is_past_trade_deadline:
await interaction.response.send_message(
f"❌ **The trade deadline has passed** (Week {current.trade_deadline}). "
f"This trade can no longer be submitted.",
ephemeral=True,
)
return
if self.builder.is_empty:
await interaction.response.send_message(
"Cannot submit empty trade. Add some moves first!", ephemeral=True
@ -433,7 +449,16 @@ class TradeAcceptanceView(discord.ui.View):
config = get_config()
current = await league_service.get_current_state()
next_week = current.week + 1 if current else 1
if not current or current.is_past_trade_deadline:
deadline_msg = (
f"❌ **The trade deadline has passed** (Week {current.trade_deadline}). "
f"This trade cannot be finalized."
if current
else "❌ Could not retrieve league state. Please try again later."
)
await interaction.followup.send(deadline_msg, ephemeral=True)
return
next_week = current.week + 1
fa_team = Team(
id=config.free_agent_team_id,