fix: roster validation now includes pending trades and fixes sWAR field name
Some checks failed
Build Docker Image / build (pull_request) Failing after 15s
Some checks failed
Build Docker Image / build (pull_request) Failing after 15s
RosterValidation used total_wara instead of total_sWAR, causing /legal to silently fail. Transaction embed and submit validation now pass next_week to validate_transaction() so pending trades are included in roster count projections. Moved lazy imports to top-level in transaction_embed.py. Fixed dropadd integration test fixtures that exceeded sWAR cap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
25ba45e529
commit
f29cccd3ab
@ -3,6 +3,7 @@ Roster service for Discord Bot v2.0
|
|||||||
|
|
||||||
Handles roster operations and validation.
|
Handles roster operations and validation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ from models.player import Player
|
|||||||
from models.transaction import RosterValidation
|
from models.transaction import RosterValidation
|
||||||
from exceptions import APIException
|
from exceptions import APIException
|
||||||
|
|
||||||
logger = logging.getLogger(f'{__name__}.RosterService')
|
logger = logging.getLogger(f"{__name__}.RosterService")
|
||||||
|
|
||||||
|
|
||||||
class RosterService:
|
class RosterService:
|
||||||
@ -21,6 +22,7 @@ class RosterService:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize roster service."""
|
"""Initialize roster service."""
|
||||||
from api.client import get_global_client
|
from api.client import get_global_client
|
||||||
|
|
||||||
self._get_client = get_global_client
|
self._get_client = get_global_client
|
||||||
logger.debug("RosterService initialized")
|
logger.debug("RosterService initialized")
|
||||||
|
|
||||||
@ -29,9 +31,7 @@ class RosterService:
|
|||||||
return await self._get_client()
|
return await self._get_client()
|
||||||
|
|
||||||
async def get_team_roster(
|
async def get_team_roster(
|
||||||
self,
|
self, team_id: int, week_type: str = "current"
|
||||||
team_id: int,
|
|
||||||
week_type: str = "current"
|
|
||||||
) -> Optional[TeamRoster]:
|
) -> Optional[TeamRoster]:
|
||||||
"""
|
"""
|
||||||
Get team roster for current or next week.
|
Get team roster for current or next week.
|
||||||
@ -47,19 +47,21 @@ class RosterService:
|
|||||||
client = await self.get_client()
|
client = await self.get_client()
|
||||||
|
|
||||||
# Use the team roster endpoint
|
# Use the team roster endpoint
|
||||||
roster_data = await client.get(f'teams/{team_id}/roster/{week_type}')
|
roster_data = await client.get(f"teams/{team_id}/roster/{week_type}")
|
||||||
|
|
||||||
if not roster_data:
|
if not roster_data:
|
||||||
logger.warning(f"No roster data found for team {team_id}, week {week_type}")
|
logger.warning(
|
||||||
|
f"No roster data found for team {team_id}, week {week_type}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Add team metadata if not present
|
# Add team metadata if not present
|
||||||
if 'team_id' not in roster_data:
|
if "team_id" not in roster_data:
|
||||||
roster_data['team_id'] = team_id
|
roster_data["team_id"] = team_id
|
||||||
|
|
||||||
# Determine week number (this might need adjustment based on API)
|
# Determine week number (this might need adjustment based on API)
|
||||||
roster_data.setdefault('week', 0) # Will need current week info
|
roster_data.setdefault("week", 0) # Will need current week info
|
||||||
roster_data.setdefault('season', 12) # Will need current season info
|
roster_data.setdefault("season", 12) # Will need current season info
|
||||||
|
|
||||||
roster = TeamRoster.from_api_data(roster_data)
|
roster = TeamRoster.from_api_data(roster_data)
|
||||||
|
|
||||||
@ -95,47 +97,64 @@ class RosterService:
|
|||||||
active_players=roster.active_count,
|
active_players=roster.active_count,
|
||||||
il_players=roster.il_count,
|
il_players=roster.il_count,
|
||||||
minor_league_players=roster.minor_league_count,
|
minor_league_players=roster.minor_league_count,
|
||||||
total_wara=roster.total_wara
|
total_sWAR=roster.total_wara,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate active roster size (typical limits)
|
# Validate active roster size (typical limits)
|
||||||
if roster.active_count > 25: # Adjust based on league rules
|
if roster.active_count > 25: # Adjust based on league rules
|
||||||
validation.is_legal = False
|
validation.is_legal = False
|
||||||
validation.errors.append(f"Too many active players: {roster.active_count}/25")
|
validation.errors.append(
|
||||||
|
f"Too many active players: {roster.active_count}/25"
|
||||||
|
)
|
||||||
elif roster.active_count < 20: # Minimum active roster
|
elif roster.active_count < 20: # Minimum active roster
|
||||||
validation.warnings.append(f"Low active player count: {roster.active_count}")
|
validation.warnings.append(
|
||||||
|
f"Low active player count: {roster.active_count}"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate total roster size
|
# Validate total roster size
|
||||||
if roster.total_players > 50: # Adjust based on league rules
|
if roster.total_players > 50: # Adjust based on league rules
|
||||||
validation.is_legal = False
|
validation.is_legal = False
|
||||||
validation.errors.append(f"Total roster too large: {roster.total_players}/50")
|
validation.errors.append(
|
||||||
|
f"Total roster too large: {roster.total_players}/50"
|
||||||
|
)
|
||||||
|
|
||||||
# Position requirements validation
|
# Position requirements validation
|
||||||
position_counts = self._count_positions(roster.active_players)
|
position_counts = self._count_positions(roster.active_players)
|
||||||
|
|
||||||
# Check catcher requirement (at least 2 catchers)
|
# Check catcher requirement (at least 2 catchers)
|
||||||
if position_counts.get('C', 0) < 2:
|
if position_counts.get("C", 0) < 2:
|
||||||
validation.warnings.append("Fewer than 2 catchers on active roster")
|
validation.warnings.append("Fewer than 2 catchers on active roster")
|
||||||
|
|
||||||
# Check pitcher requirements (at least 10 pitchers)
|
# Check pitcher requirements (at least 10 pitchers)
|
||||||
pitcher_count = position_counts.get('SP', 0) + position_counts.get('RP', 0) + position_counts.get('P', 0)
|
pitcher_count = (
|
||||||
|
position_counts.get("SP", 0)
|
||||||
|
+ position_counts.get("RP", 0)
|
||||||
|
+ position_counts.get("P", 0)
|
||||||
|
)
|
||||||
if pitcher_count < 10:
|
if pitcher_count < 10:
|
||||||
validation.warnings.append(f"Fewer than 10 pitchers on active roster: {pitcher_count}")
|
validation.warnings.append(
|
||||||
|
f"Fewer than 10 pitchers on active roster: {pitcher_count}"
|
||||||
|
)
|
||||||
|
|
||||||
# WARA validation (if there are limits)
|
# WARA validation (if there are limits)
|
||||||
if validation.total_wara > 100: # Adjust based on league rules
|
if validation.total_sWAR > 100: # Adjust based on league rules
|
||||||
validation.warnings.append(f"High WARA total: {validation.total_wara:.2f}")
|
validation.warnings.append(
|
||||||
elif validation.total_wara < 20:
|
f"High WARA total: {validation.total_sWAR:.2f}"
|
||||||
validation.warnings.append(f"Low WARA total: {validation.total_wara:.2f}")
|
)
|
||||||
|
elif validation.total_sWAR < 20:
|
||||||
|
validation.warnings.append(
|
||||||
|
f"Low WARA total: {validation.total_sWAR:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f"Validated roster: legal={validation.is_legal}, {len(validation.errors)} errors, {len(validation.warnings)} warnings")
|
logger.debug(
|
||||||
|
f"Validated roster: legal={validation.is_legal}, {len(validation.errors)} errors, {len(validation.warnings)} warnings"
|
||||||
|
)
|
||||||
return validation
|
return validation
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error validating roster: {e}")
|
logger.error(f"Error validating roster: {e}")
|
||||||
return RosterValidation(
|
return RosterValidation(
|
||||||
is_legal=False,
|
is_legal=False, errors=[f"Validation error: {str(e)}"]
|
||||||
errors=[f"Validation error: {str(e)}"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _count_positions(self, players: List[Player]) -> Dict[str, int]:
|
def _count_positions(self, players: List[Player]) -> Dict[str, int]:
|
||||||
@ -160,25 +179,29 @@ class RosterService:
|
|||||||
position_counts = self._count_positions(roster.active_players)
|
position_counts = self._count_positions(roster.active_players)
|
||||||
|
|
||||||
# Group positions
|
# Group positions
|
||||||
catchers = position_counts.get('C', 0)
|
catchers = position_counts.get("C", 0)
|
||||||
infielders = sum(position_counts.get(pos, 0) for pos in ['1B', '2B', '3B', 'SS', 'IF'])
|
infielders = sum(
|
||||||
outfielders = sum(position_counts.get(pos, 0) for pos in ['LF', 'CF', 'RF', 'OF'])
|
position_counts.get(pos, 0) for pos in ["1B", "2B", "3B", "SS", "IF"]
|
||||||
pitchers = sum(position_counts.get(pos, 0) for pos in ['SP', 'RP', 'P'])
|
)
|
||||||
dh = position_counts.get('DH', 0)
|
outfielders = sum(
|
||||||
|
position_counts.get(pos, 0) for pos in ["LF", "CF", "RF", "OF"]
|
||||||
|
)
|
||||||
|
pitchers = sum(position_counts.get(pos, 0) for pos in ["SP", "RP", "P"])
|
||||||
|
dh = position_counts.get("DH", 0)
|
||||||
|
|
||||||
summary = {
|
summary = {
|
||||||
'total_active': roster.active_count,
|
"total_active": roster.active_count,
|
||||||
'total_il': roster.il_count,
|
"total_il": roster.il_count,
|
||||||
'total_minor': roster.minor_league_count,
|
"total_minor": roster.minor_league_count,
|
||||||
'total_wara': roster.total_wara,
|
"total_wara": roster.total_wara,
|
||||||
'positions': {
|
"positions": {
|
||||||
'catchers': catchers,
|
"catchers": catchers,
|
||||||
'infielders': infielders,
|
"infielders": infielders,
|
||||||
'outfielders': outfielders,
|
"outfielders": outfielders,
|
||||||
'pitchers': pitchers,
|
"pitchers": pitchers,
|
||||||
'dh': dh
|
"dh": dh,
|
||||||
},
|
},
|
||||||
'detailed_positions': position_counts
|
"detailed_positions": position_counts,
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary
|
return summary
|
||||||
|
|||||||
@ -3,6 +3,7 @@ Integration tests for /dropadd functionality
|
|||||||
|
|
||||||
Tests complete workflows from command invocation through transaction submission.
|
Tests complete workflows from command invocation through transaction submission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -12,13 +13,10 @@ from services.transaction_builder import (
|
|||||||
TransactionBuilder,
|
TransactionBuilder,
|
||||||
TransactionMove,
|
TransactionMove,
|
||||||
get_transaction_builder,
|
get_transaction_builder,
|
||||||
clear_transaction_builder
|
clear_transaction_builder,
|
||||||
)
|
)
|
||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
from views.transaction_embed import (
|
from views.transaction_embed import TransactionEmbedView, SubmitConfirmationModal
|
||||||
TransactionEmbedView,
|
|
||||||
SubmitConfirmationModal
|
|
||||||
)
|
|
||||||
from models.team import Team
|
from models.team import Team
|
||||||
from models.player import Player
|
from models.player import Player
|
||||||
from models.roster import TeamRoster
|
from models.roster import TeamRoster
|
||||||
@ -63,7 +61,9 @@ class TestDropAddIntegration:
|
|||||||
mock_message.embeds[0].title = "📋 Transaction Builder"
|
mock_message.embeds[0].title = "📋 Transaction Builder"
|
||||||
mock_message.edit = AsyncMock()
|
mock_message.edit = AsyncMock()
|
||||||
|
|
||||||
interaction.channel.history.return_value.__aiter__ = AsyncMock(return_value=iter([mock_message]))
|
interaction.channel.history.return_value.__aiter__ = AsyncMock(
|
||||||
|
return_value=iter([mock_message])
|
||||||
|
)
|
||||||
|
|
||||||
return interaction
|
return interaction
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class TestDropAddIntegration:
|
|||||||
return [
|
return [
|
||||||
PlayerFactory.mike_trout(),
|
PlayerFactory.mike_trout(),
|
||||||
PlayerFactory.ronald_acuna(),
|
PlayerFactory.ronald_acuna(),
|
||||||
PlayerFactory.mookie_betts()
|
PlayerFactory.mookie_betts(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -91,75 +91,77 @@ class TestDropAddIntegration:
|
|||||||
# Create 24 ML players (under limit)
|
# Create 24 ML players (under limit)
|
||||||
ml_players = []
|
ml_players = []
|
||||||
for i in range(24):
|
for i in range(24):
|
||||||
ml_players.append(Player(
|
ml_players.append(
|
||||||
id=1000 + i,
|
Player(
|
||||||
name=f'ML Player {i}',
|
id=1000 + i,
|
||||||
wara=3.0 + i * 0.1,
|
name=f"ML Player {i}",
|
||||||
season=13,
|
wara=1.0,
|
||||||
team_id=499,
|
season=13,
|
||||||
team=None,
|
team_id=499,
|
||||||
image=None,
|
team=None,
|
||||||
image2=None,
|
image=None,
|
||||||
vanity_card=None,
|
image2=None,
|
||||||
headshot=None,
|
vanity_card=None,
|
||||||
pos_1='OF',
|
headshot=None,
|
||||||
pitcher_injury=None,
|
pos_1="OF",
|
||||||
injury_rating=None,
|
pitcher_injury=None,
|
||||||
il_return=None,
|
injury_rating=None,
|
||||||
demotion_week=None,
|
il_return=None,
|
||||||
last_game=None,
|
demotion_week=None,
|
||||||
last_game2=None,
|
last_game=None,
|
||||||
strat_code=None,
|
last_game2=None,
|
||||||
bbref_id=None,
|
strat_code=None,
|
||||||
sbaplayer=None
|
bbref_id=None,
|
||||||
))
|
sbaplayer=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Create 4 MiL players (under 6 limit to allow adding)
|
# Create 4 MiL players (under 6 limit to allow adding)
|
||||||
mil_players = []
|
mil_players = []
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
mil_players.append(Player(
|
mil_players.append(
|
||||||
id=2000 + i,
|
Player(
|
||||||
name=f'MiL Player {i}',
|
id=2000 + i,
|
||||||
wara=1.0 + i * 0.1,
|
name=f"MiL Player {i}",
|
||||||
season=13,
|
wara=1.0 + i * 0.1,
|
||||||
team_id=499,
|
season=13,
|
||||||
team=None,
|
team_id=499,
|
||||||
image=None,
|
team=None,
|
||||||
image2=None,
|
image=None,
|
||||||
vanity_card=None,
|
image2=None,
|
||||||
headshot=None,
|
vanity_card=None,
|
||||||
pos_1='OF',
|
headshot=None,
|
||||||
pitcher_injury=None,
|
pos_1="OF",
|
||||||
injury_rating=None,
|
pitcher_injury=None,
|
||||||
il_return=None,
|
injury_rating=None,
|
||||||
demotion_week=None,
|
il_return=None,
|
||||||
last_game=None,
|
demotion_week=None,
|
||||||
last_game2=None,
|
last_game=None,
|
||||||
strat_code=None,
|
last_game2=None,
|
||||||
bbref_id=None,
|
strat_code=None,
|
||||||
sbaplayer=None
|
bbref_id=None,
|
||||||
))
|
sbaplayer=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return TeamRoster(
|
return TeamRoster(
|
||||||
team_id=499,
|
team_id=499,
|
||||||
team_abbrev='TST',
|
team_abbrev="TST",
|
||||||
week=10,
|
week=10,
|
||||||
season=13,
|
season=13,
|
||||||
active_players=ml_players,
|
active_players=ml_players,
|
||||||
minor_league_players=mil_players
|
minor_league_players=mil_players,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_current_state(self):
|
def mock_current_state(self):
|
||||||
"""Create mock current league state."""
|
"""Create mock current league state."""
|
||||||
return Current(
|
return Current(week=10, season=13, freeze=False)
|
||||||
week=10,
|
|
||||||
season=13,
|
|
||||||
freeze=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_complete_single_move_workflow(self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster):
|
async def test_complete_single_move_workflow(
|
||||||
|
self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster
|
||||||
|
):
|
||||||
"""Test complete workflow for single move transaction.
|
"""Test complete workflow for single move transaction.
|
||||||
|
|
||||||
Verifies that when a player and destination are provided to /dropadd,
|
Verifies that when a player and destination are provided to /dropadd,
|
||||||
@ -173,23 +175,42 @@ class TestDropAddIntegration:
|
|||||||
# Clear any existing builders
|
# Clear any existing builders
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
|
|
||||||
with patch('commands.transactions.dropadd.validate_user_has_team') as mock_validate:
|
with patch(
|
||||||
with patch('commands.transactions.dropadd.player_service') as mock_player_service:
|
"commands.transactions.dropadd.validate_user_has_team"
|
||||||
with patch('services.transaction_builder.roster_service') as mock_roster_service:
|
) as mock_validate:
|
||||||
with patch('services.transaction_builder.transaction_service') as mock_tx_service:
|
with patch(
|
||||||
|
"commands.transactions.dropadd.player_service"
|
||||||
|
) as mock_player_service:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.roster_service"
|
||||||
|
) as mock_roster_service:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.transaction_service"
|
||||||
|
) as mock_tx_service:
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
mock_validate.return_value = mock_team # validate_user_has_team returns team
|
mock_validate.return_value = (
|
||||||
mock_player_service.search_players = AsyncMock(return_value=[mock_players[0]]) # Mike Trout
|
mock_team # validate_user_has_team returns team
|
||||||
mock_roster_service.get_current_roster = AsyncMock(return_value=mock_roster)
|
)
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
mock_player_service.search_players = AsyncMock(
|
||||||
|
return_value=[mock_players[0]]
|
||||||
|
) # Mike Trout
|
||||||
|
mock_roster_service.get_current_roster = AsyncMock(
|
||||||
|
return_value=mock_roster
|
||||||
|
)
|
||||||
|
mock_tx_service.get_team_transactions = AsyncMock(
|
||||||
|
return_value=[]
|
||||||
|
)
|
||||||
# Mock the new pending transaction check
|
# Mock the new pending transaction check
|
||||||
mock_tx_service.is_player_in_pending_transaction = AsyncMock(return_value=(False, None))
|
mock_tx_service.is_player_in_pending_transaction = AsyncMock(
|
||||||
|
return_value=(False, None)
|
||||||
|
)
|
||||||
|
|
||||||
# Execute /dropadd command with quick move
|
# Execute /dropadd command with quick move
|
||||||
await commands_cog.dropadd.callback(commands_cog,
|
await commands_cog.dropadd.callback(
|
||||||
|
commands_cog,
|
||||||
mock_interaction,
|
mock_interaction,
|
||||||
player='Mike Trout',
|
player="Mike Trout",
|
||||||
destination='ml'
|
destination="ml",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify command execution
|
# Verify command execution
|
||||||
@ -197,12 +218,14 @@ class TestDropAddIntegration:
|
|||||||
mock_interaction.followup.send.assert_called_once()
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
|
||||||
# Get the builder that was created
|
# Get the builder that was created
|
||||||
builder = get_transaction_builder(mock_interaction.user.id, mock_team)
|
builder = get_transaction_builder(
|
||||||
|
mock_interaction.user.id, mock_team
|
||||||
|
)
|
||||||
|
|
||||||
# Verify the move was added
|
# Verify the move was added
|
||||||
assert builder.move_count == 1
|
assert builder.move_count == 1
|
||||||
move = builder.moves[0]
|
move = builder.moves[0]
|
||||||
assert move.player.name == 'Mike Trout'
|
assert move.player.name == "Mike Trout"
|
||||||
# Note: TransactionMove no longer has 'action' field
|
# Note: TransactionMove no longer has 'action' field
|
||||||
assert move.to_roster == RosterType.MAJOR_LEAGUE
|
assert move.to_roster == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
@ -212,7 +235,9 @@ class TestDropAddIntegration:
|
|||||||
assert validation.major_league_count == 25 # 24 + 1
|
assert validation.major_league_count == 25 # 24 + 1
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_complete_multi_move_workflow(self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster):
|
async def test_complete_multi_move_workflow(
|
||||||
|
self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster
|
||||||
|
):
|
||||||
"""Test complete workflow for multi-move transaction.
|
"""Test complete workflow for multi-move transaction.
|
||||||
|
|
||||||
Verifies that manually adding multiple moves to the transaction builder
|
Verifies that manually adding multiple moves to the transaction builder
|
||||||
@ -220,32 +245,42 @@ class TestDropAddIntegration:
|
|||||||
"""
|
"""
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
|
|
||||||
with patch('commands.transactions.dropadd.validate_user_has_team') as mock_validate:
|
with patch(
|
||||||
with patch('services.transaction_builder.roster_service') as mock_roster_service:
|
"commands.transactions.dropadd.validate_user_has_team"
|
||||||
with patch('services.transaction_builder.transaction_service') as mock_tx_service:
|
) as mock_validate:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.roster_service"
|
||||||
|
) as mock_roster_service:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.transaction_service"
|
||||||
|
) as mock_tx_service:
|
||||||
mock_validate.return_value = mock_team
|
mock_validate.return_value = mock_team
|
||||||
mock_roster_service.get_current_roster = AsyncMock(return_value=mock_roster)
|
mock_roster_service.get_current_roster = AsyncMock(
|
||||||
|
return_value=mock_roster
|
||||||
|
)
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Start with /dropadd command
|
# Start with /dropadd command
|
||||||
await commands_cog.dropadd.callback(commands_cog, mock_interaction)
|
await commands_cog.dropadd.callback(commands_cog, mock_interaction)
|
||||||
|
|
||||||
# Get the builder
|
# Get the builder
|
||||||
builder = get_transaction_builder(mock_interaction.user.id, mock_team)
|
builder = get_transaction_builder(
|
||||||
|
mock_interaction.user.id, mock_team
|
||||||
|
)
|
||||||
|
|
||||||
# Manually add multiple moves (simulating UI interactions)
|
# Manually add multiple moves (simulating UI interactions)
|
||||||
add_move = TransactionMove(
|
add_move = TransactionMove(
|
||||||
player=mock_players[0], # Mike Trout
|
player=mock_players[0], # Mike Trout
|
||||||
from_roster=RosterType.FREE_AGENCY,
|
from_roster=RosterType.FREE_AGENCY,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE,
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_team=mock_team
|
to_team=mock_team,
|
||||||
)
|
)
|
||||||
|
|
||||||
drop_move = TransactionMove(
|
drop_move = TransactionMove(
|
||||||
player=mock_players[1], # Ronald Acuna Jr.
|
player=mock_players[1], # Ronald Acuna Jr.
|
||||||
from_roster=RosterType.MAJOR_LEAGUE,
|
from_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_roster=RosterType.FREE_AGENCY,
|
to_roster=RosterType.FREE_AGENCY,
|
||||||
from_team=mock_team
|
from_team=mock_team,
|
||||||
)
|
)
|
||||||
|
|
||||||
await builder.add_move(add_move, check_pending_transactions=False)
|
await builder.add_move(add_move, check_pending_transactions=False)
|
||||||
@ -258,7 +293,15 @@ class TestDropAddIntegration:
|
|||||||
assert validation.major_league_count == 24 # 24 + 1 - 1 = 24
|
assert validation.major_league_count == 24 # 24 + 1 - 1 = 24
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_complete_submission_workflow(self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster, mock_current_state):
|
async def test_complete_submission_workflow(
|
||||||
|
self,
|
||||||
|
commands_cog,
|
||||||
|
mock_interaction,
|
||||||
|
mock_team,
|
||||||
|
mock_players,
|
||||||
|
mock_roster,
|
||||||
|
mock_current_state,
|
||||||
|
):
|
||||||
"""Test complete transaction submission workflow.
|
"""Test complete transaction submission workflow.
|
||||||
|
|
||||||
Verifies that submitting a transaction via the builder creates
|
Verifies that submitting a transaction via the builder creates
|
||||||
@ -266,10 +309,16 @@ class TestDropAddIntegration:
|
|||||||
"""
|
"""
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
|
|
||||||
with patch('services.transaction_builder.roster_service') as mock_roster_service:
|
with patch(
|
||||||
with patch('services.transaction_builder.transaction_service') as mock_tx_service:
|
"services.transaction_builder.roster_service"
|
||||||
|
) as mock_roster_service:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.transaction_service"
|
||||||
|
) as mock_tx_service:
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
mock_roster_service.get_current_roster = AsyncMock(return_value=mock_roster)
|
mock_roster_service.get_current_roster = AsyncMock(
|
||||||
|
return_value=mock_roster
|
||||||
|
)
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Create builder and add move
|
# Create builder and add move
|
||||||
@ -278,7 +327,7 @@ class TestDropAddIntegration:
|
|||||||
player=mock_players[0],
|
player=mock_players[0],
|
||||||
from_roster=RosterType.FREE_AGENCY,
|
from_roster=RosterType.FREE_AGENCY,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE,
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_team=mock_team
|
to_team=mock_team,
|
||||||
)
|
)
|
||||||
await builder.add_move(move, check_pending_transactions=False)
|
await builder.add_move(move, check_pending_transactions=False)
|
||||||
|
|
||||||
@ -289,14 +338,15 @@ class TestDropAddIntegration:
|
|||||||
assert len(transactions) == 1
|
assert len(transactions) == 1
|
||||||
transaction = transactions[0]
|
transaction = transactions[0]
|
||||||
assert isinstance(transaction, Transaction)
|
assert isinstance(transaction, Transaction)
|
||||||
assert transaction.player.name == 'Mike Trout'
|
assert transaction.player.name == "Mike Trout"
|
||||||
assert transaction.week == 11
|
assert transaction.week == 11
|
||||||
assert transaction.season == 13
|
assert transaction.season == 13
|
||||||
assert "Season-013-Week-11-" in transaction.moveid
|
assert "Season-013-Week-11-" in transaction.moveid
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_submission_modal_workflow(self, mock_interaction, mock_team, mock_players, mock_roster, mock_current_state):
|
async def test_submission_modal_workflow(
|
||||||
|
self, mock_interaction, mock_team, mock_players, mock_roster, mock_current_state
|
||||||
|
):
|
||||||
"""Test submission confirmation modal workflow.
|
"""Test submission confirmation modal workflow.
|
||||||
|
|
||||||
Verifies that the SubmitConfirmationModal properly:
|
Verifies that the SubmitConfirmationModal properly:
|
||||||
@ -305,42 +355,70 @@ class TestDropAddIntegration:
|
|||||||
3. Submits transactions
|
3. Submits transactions
|
||||||
4. Posts success message
|
4. Posts success message
|
||||||
|
|
||||||
Note: The modal imports services dynamically inside on_submit(),
|
Note: The modal uses services imported at the top of views/transaction_embed.py,
|
||||||
so we patch them where they're imported from (services.X module).
|
so we patch them where they're used (views.transaction_embed module).
|
||||||
|
|
||||||
Note: Discord.py's TextInput.value is a read-only property, so we
|
Note: Discord.py's TextInput.value is a read-only property, so we
|
||||||
replace the entire confirmation attribute with a MagicMock.
|
replace the entire confirmation attribute with a MagicMock.
|
||||||
"""
|
"""
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
|
|
||||||
with patch('services.transaction_builder.roster_service') as mock_roster_service:
|
with patch(
|
||||||
with patch('services.transaction_builder.transaction_service') as mock_tx_service:
|
"services.transaction_builder.roster_service"
|
||||||
with patch('services.league_service.league_service') as mock_league_service:
|
) as mock_roster_service:
|
||||||
with patch('services.transaction_service.transaction_service') as mock_view_tx_service:
|
with patch(
|
||||||
with patch('utils.transaction_logging.post_transaction_to_log') as mock_post_log:
|
"services.transaction_builder.transaction_service"
|
||||||
mock_roster_service.get_current_roster = AsyncMock(return_value=mock_roster)
|
) as mock_tx_service:
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
with patch(
|
||||||
mock_league_service.get_current_state = AsyncMock(return_value=mock_current_state)
|
"views.transaction_embed.league_service"
|
||||||
|
) as mock_league_service:
|
||||||
|
with patch(
|
||||||
|
"views.transaction_embed.transaction_service"
|
||||||
|
) as mock_view_tx_service:
|
||||||
|
with patch(
|
||||||
|
"utils.transaction_logging.post_transaction_to_log"
|
||||||
|
) as mock_post_log:
|
||||||
|
mock_roster_service.get_current_roster = AsyncMock(
|
||||||
|
return_value=mock_roster
|
||||||
|
)
|
||||||
|
mock_tx_service.get_team_transactions = AsyncMock(
|
||||||
|
return_value=[]
|
||||||
|
)
|
||||||
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
mock_post_log.return_value = None
|
mock_post_log.return_value = None
|
||||||
|
|
||||||
# Create builder with move
|
# Create builder with move
|
||||||
builder = get_transaction_builder(mock_interaction.user.id, mock_team)
|
builder = get_transaction_builder(
|
||||||
|
mock_interaction.user.id, mock_team
|
||||||
|
)
|
||||||
move = TransactionMove(
|
move = TransactionMove(
|
||||||
player=mock_players[0],
|
player=mock_players[0],
|
||||||
from_roster=RosterType.FREE_AGENCY,
|
from_roster=RosterType.FREE_AGENCY,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE,
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_team=mock_team
|
to_team=mock_team,
|
||||||
|
)
|
||||||
|
await builder.add_move(
|
||||||
|
move, check_pending_transactions=False
|
||||||
)
|
)
|
||||||
await builder.add_move(move, check_pending_transactions=False)
|
|
||||||
|
|
||||||
# Submit transactions first to get move IDs
|
# Submit transactions first to get move IDs
|
||||||
transactions = await builder.submit_transaction(week=mock_current_state.week + 1)
|
transactions = await builder.submit_transaction(
|
||||||
mock_view_tx_service.create_transaction_batch = AsyncMock(return_value=transactions)
|
week=mock_current_state.week + 1
|
||||||
|
)
|
||||||
|
mock_view_tx_service.create_transaction_batch = AsyncMock(
|
||||||
|
return_value=transactions
|
||||||
|
)
|
||||||
|
|
||||||
# Reset the builder and add move again for modal test
|
# Reset the builder and add move again for modal test
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
builder = get_transaction_builder(mock_interaction.user.id, mock_team)
|
builder = get_transaction_builder(
|
||||||
await builder.add_move(move, check_pending_transactions=False)
|
mock_interaction.user.id, mock_team
|
||||||
|
)
|
||||||
|
await builder.add_move(
|
||||||
|
move, check_pending_transactions=False
|
||||||
|
)
|
||||||
|
|
||||||
# Create the modal
|
# Create the modal
|
||||||
modal = SubmitConfirmationModal(builder)
|
modal = SubmitConfirmationModal(builder)
|
||||||
@ -348,14 +426,16 @@ class TestDropAddIntegration:
|
|||||||
# Replace the entire confirmation input with a mock that has .value
|
# Replace the entire confirmation input with a mock that has .value
|
||||||
# Discord.py's TextInput.value is read-only, so we can't patch it
|
# Discord.py's TextInput.value is read-only, so we can't patch it
|
||||||
mock_confirmation = MagicMock()
|
mock_confirmation = MagicMock()
|
||||||
mock_confirmation.value = 'CONFIRM'
|
mock_confirmation.value = "CONFIRM"
|
||||||
modal.confirmation = mock_confirmation
|
modal.confirmation = mock_confirmation
|
||||||
|
|
||||||
await modal.on_submit(mock_interaction)
|
await modal.on_submit(mock_interaction)
|
||||||
|
|
||||||
# Verify submission process
|
# Verify submission process
|
||||||
mock_league_service.get_current_state.assert_called()
|
mock_league_service.get_current_state.assert_called()
|
||||||
mock_interaction.response.defer.assert_called_once_with(ephemeral=True)
|
mock_interaction.response.defer.assert_called_once_with(
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
mock_interaction.followup.send.assert_called_once()
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
|
||||||
# Verify success message
|
# Verify success message
|
||||||
@ -365,7 +445,9 @@ class TestDropAddIntegration:
|
|||||||
assert "Move ID:" in success_msg
|
assert "Move ID:" in success_msg
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_error_handling_workflow(self, commands_cog, mock_interaction, mock_team):
|
async def test_error_handling_workflow(
|
||||||
|
self, commands_cog, mock_interaction, mock_team
|
||||||
|
):
|
||||||
"""Test error handling throughout the workflow.
|
"""Test error handling throughout the workflow.
|
||||||
|
|
||||||
Verifies that when validate_user_has_team raises an error,
|
Verifies that when validate_user_has_team raises an error,
|
||||||
@ -377,7 +459,9 @@ class TestDropAddIntegration:
|
|||||||
"""
|
"""
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
|
|
||||||
with patch('commands.transactions.dropadd.validate_user_has_team') as mock_validate:
|
with patch(
|
||||||
|
"commands.transactions.dropadd.validate_user_has_team"
|
||||||
|
) as mock_validate:
|
||||||
# Test API error handling
|
# Test API error handling
|
||||||
mock_validate.side_effect = Exception("API Error")
|
mock_validate.side_effect = Exception("API Error")
|
||||||
|
|
||||||
@ -392,7 +476,9 @@ class TestDropAddIntegration:
|
|||||||
mock_interaction.response.defer.assert_called_once()
|
mock_interaction.response.defer.assert_called_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_roster_validation_workflow(self, commands_cog, mock_interaction, mock_team, mock_players):
|
async def test_roster_validation_workflow(
|
||||||
|
self, commands_cog, mock_interaction, mock_team, mock_players
|
||||||
|
):
|
||||||
"""Test roster validation throughout workflow.
|
"""Test roster validation throughout workflow.
|
||||||
|
|
||||||
Verifies that the transaction builder correctly validates roster limits
|
Verifies that the transaction builder correctly validates roster limits
|
||||||
@ -403,40 +489,48 @@ class TestDropAddIntegration:
|
|||||||
# Create roster at limit (26 ML players for week 10)
|
# Create roster at limit (26 ML players for week 10)
|
||||||
ml_players = []
|
ml_players = []
|
||||||
for i in range(26):
|
for i in range(26):
|
||||||
ml_players.append(Player(
|
ml_players.append(
|
||||||
id=1000 + i,
|
Player(
|
||||||
name=f'ML Player {i}',
|
id=1000 + i,
|
||||||
wara=3.0 + i * 0.1,
|
name=f"ML Player {i}",
|
||||||
season=13,
|
wara=1.0,
|
||||||
team_id=499,
|
season=13,
|
||||||
team=None,
|
team_id=499,
|
||||||
image=None,
|
team=None,
|
||||||
image2=None,
|
image=None,
|
||||||
vanity_card=None,
|
image2=None,
|
||||||
headshot=None,
|
vanity_card=None,
|
||||||
pos_1='OF',
|
headshot=None,
|
||||||
pitcher_injury=None,
|
pos_1="OF",
|
||||||
injury_rating=None,
|
pitcher_injury=None,
|
||||||
il_return=None,
|
injury_rating=None,
|
||||||
demotion_week=None,
|
il_return=None,
|
||||||
last_game=None,
|
demotion_week=None,
|
||||||
last_game2=None,
|
last_game=None,
|
||||||
strat_code=None,
|
last_game2=None,
|
||||||
bbref_id=None,
|
strat_code=None,
|
||||||
sbaplayer=None
|
bbref_id=None,
|
||||||
))
|
sbaplayer=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
full_roster = TeamRoster(
|
full_roster = TeamRoster(
|
||||||
team_id=499,
|
team_id=499,
|
||||||
team_abbrev='TST',
|
team_abbrev="TST",
|
||||||
week=10,
|
week=10,
|
||||||
season=13,
|
season=13,
|
||||||
active_players=ml_players
|
active_players=ml_players,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch('services.transaction_builder.roster_service') as mock_roster_service:
|
with patch(
|
||||||
with patch('services.transaction_builder.transaction_service') as mock_tx_service:
|
"services.transaction_builder.roster_service"
|
||||||
mock_roster_service.get_current_roster = AsyncMock(return_value=full_roster)
|
) as mock_roster_service:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.transaction_service"
|
||||||
|
) as mock_tx_service:
|
||||||
|
mock_roster_service.get_current_roster = AsyncMock(
|
||||||
|
return_value=full_roster
|
||||||
|
)
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Create builder and try to add player (should exceed limit)
|
# Create builder and try to add player (should exceed limit)
|
||||||
@ -445,7 +539,7 @@ class TestDropAddIntegration:
|
|||||||
player=mock_players[0],
|
player=mock_players[0],
|
||||||
from_roster=RosterType.FREE_AGENCY,
|
from_roster=RosterType.FREE_AGENCY,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE,
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_team=mock_team
|
to_team=mock_team,
|
||||||
)
|
)
|
||||||
await builder.add_move(move, check_pending_transactions=False)
|
await builder.add_move(move, check_pending_transactions=False)
|
||||||
|
|
||||||
@ -459,7 +553,9 @@ class TestDropAddIntegration:
|
|||||||
assert "Drop 1 ML player" in validation.suggestions[0]
|
assert "Drop 1 ML player" in validation.suggestions[0]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_builder_persistence_workflow(self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster):
|
async def test_builder_persistence_workflow(
|
||||||
|
self, commands_cog, mock_interaction, mock_team, mock_players, mock_roster
|
||||||
|
):
|
||||||
"""Test that transaction builder persists across command calls.
|
"""Test that transaction builder persists across command calls.
|
||||||
|
|
||||||
Verifies that calling /dropadd multiple times uses the same
|
Verifies that calling /dropadd multiple times uses the same
|
||||||
@ -467,33 +563,44 @@ class TestDropAddIntegration:
|
|||||||
"""
|
"""
|
||||||
clear_transaction_builder(mock_interaction.user.id)
|
clear_transaction_builder(mock_interaction.user.id)
|
||||||
|
|
||||||
with patch('commands.transactions.dropadd.validate_user_has_team') as mock_validate:
|
with patch(
|
||||||
with patch('services.transaction_builder.roster_service') as mock_roster_service:
|
"commands.transactions.dropadd.validate_user_has_team"
|
||||||
with patch('services.transaction_builder.transaction_service') as mock_tx_service:
|
) as mock_validate:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.roster_service"
|
||||||
|
) as mock_roster_service:
|
||||||
|
with patch(
|
||||||
|
"services.transaction_builder.transaction_service"
|
||||||
|
) as mock_tx_service:
|
||||||
mock_validate.return_value = mock_team
|
mock_validate.return_value = mock_team
|
||||||
mock_roster_service.get_current_roster = AsyncMock(return_value=mock_roster)
|
mock_roster_service.get_current_roster = AsyncMock(
|
||||||
|
return_value=mock_roster
|
||||||
|
)
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_team_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# First command call
|
# First command call
|
||||||
await commands_cog.dropadd.callback(commands_cog, mock_interaction)
|
await commands_cog.dropadd.callback(commands_cog, mock_interaction)
|
||||||
builder1 = get_transaction_builder(mock_interaction.user.id, mock_team)
|
builder1 = get_transaction_builder(
|
||||||
|
mock_interaction.user.id, mock_team
|
||||||
|
)
|
||||||
|
|
||||||
# Add a move
|
# Add a move
|
||||||
move = TransactionMove(
|
move = TransactionMove(
|
||||||
player=mock_players[0],
|
player=mock_players[0],
|
||||||
from_roster=RosterType.FREE_AGENCY,
|
from_roster=RosterType.FREE_AGENCY,
|
||||||
to_roster=RosterType.MAJOR_LEAGUE,
|
to_roster=RosterType.MAJOR_LEAGUE,
|
||||||
to_team=mock_team
|
to_team=mock_team,
|
||||||
)
|
)
|
||||||
await builder1.add_move(move, check_pending_transactions=False)
|
await builder1.add_move(move, check_pending_transactions=False)
|
||||||
assert builder1.move_count == 1
|
assert builder1.move_count == 1
|
||||||
|
|
||||||
# Second command call should get same builder
|
# Second command call should get same builder
|
||||||
await commands_cog.dropadd.callback(commands_cog, mock_interaction)
|
await commands_cog.dropadd.callback(commands_cog, mock_interaction)
|
||||||
builder2 = get_transaction_builder(mock_interaction.user.id, mock_team)
|
builder2 = get_transaction_builder(
|
||||||
|
mock_interaction.user.id, mock_team
|
||||||
|
)
|
||||||
|
|
||||||
# Should be same instance with same moves
|
# Should be same instance with same moves
|
||||||
assert builder1 is builder2
|
assert builder1 is builder2
|
||||||
assert builder2.move_count == 1
|
assert builder2.move_count == 1
|
||||||
assert builder2.moves[0].player.name == 'Mike Trout'
|
assert builder2.moves[0].player.name == "Mike Trout"
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ Tests for Transaction Embed Views
|
|||||||
|
|
||||||
Validates Discord UI components, modals, and interactive elements.
|
Validates Discord UI components, modals, and interactive elements.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
import discord
|
import discord
|
||||||
@ -12,12 +13,12 @@ from views.transaction_embed import (
|
|||||||
RemoveMoveView,
|
RemoveMoveView,
|
||||||
RemoveMoveSelect,
|
RemoveMoveSelect,
|
||||||
SubmitConfirmationModal,
|
SubmitConfirmationModal,
|
||||||
create_transaction_embed
|
create_transaction_embed,
|
||||||
)
|
)
|
||||||
from services.transaction_builder import (
|
from services.transaction_builder import (
|
||||||
TransactionBuilder,
|
TransactionBuilder,
|
||||||
TransactionMove,
|
TransactionMove,
|
||||||
RosterValidationResult
|
RosterValidationResult,
|
||||||
)
|
)
|
||||||
from models.team import Team, RosterType
|
from models.team import Team, RosterType
|
||||||
from models.player import Player
|
from models.player import Player
|
||||||
@ -29,7 +30,13 @@ class TestTransactionEmbedView:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_builder(self):
|
def mock_builder(self):
|
||||||
"""Create mock TransactionBuilder."""
|
"""Create mock TransactionBuilder."""
|
||||||
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
|
team = Team(
|
||||||
|
id=499,
|
||||||
|
abbrev="WV",
|
||||||
|
sname="Black Bears",
|
||||||
|
lname="West Virginia Black Bears",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
builder = MagicMock(spec=TransactionBuilder)
|
builder = MagicMock(spec=TransactionBuilder)
|
||||||
builder.team = team
|
builder.team = team
|
||||||
builder.user_id = 123456789
|
builder.user_id = 123456789
|
||||||
@ -74,10 +81,12 @@ class TestTransactionEmbedView:
|
|||||||
mock_interaction.response.send_message.assert_called_once()
|
mock_interaction.response.send_message.assert_called_once()
|
||||||
call_args = mock_interaction.response.send_message.call_args
|
call_args = mock_interaction.response.send_message.call_args
|
||||||
assert "don't have permission" in call_args[0][0]
|
assert "don't have permission" in call_args[0][0]
|
||||||
assert call_args[1]['ephemeral'] is True
|
assert call_args[1]["ephemeral"] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_remove_move_button_empty_builder(self, mock_builder, mock_interaction):
|
async def test_remove_move_button_empty_builder(
|
||||||
|
self, mock_builder, mock_interaction
|
||||||
|
):
|
||||||
"""Test remove move button with empty builder."""
|
"""Test remove move button with empty builder."""
|
||||||
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
||||||
view.builder.is_empty = True
|
view.builder.is_empty = True
|
||||||
@ -87,7 +96,7 @@ class TestTransactionEmbedView:
|
|||||||
mock_interaction.response.send_message.assert_called_once()
|
mock_interaction.response.send_message.assert_called_once()
|
||||||
call_args = mock_interaction.response.send_message.call_args
|
call_args = mock_interaction.response.send_message.call_args
|
||||||
assert "No moves to remove" in call_args[0][0]
|
assert "No moves to remove" in call_args[0][0]
|
||||||
assert call_args[1]['ephemeral'] is True
|
assert call_args[1]["ephemeral"] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_remove_move_button_with_moves(self, mock_builder, mock_interaction):
|
async def test_remove_move_button_with_moves(self, mock_builder, mock_interaction):
|
||||||
@ -95,7 +104,9 @@ class TestTransactionEmbedView:
|
|||||||
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
||||||
view.builder.is_empty = False
|
view.builder.is_empty = False
|
||||||
|
|
||||||
with patch('views.transaction_embed.create_transaction_embed') as mock_create_embed:
|
with patch(
|
||||||
|
"views.transaction_embed.create_transaction_embed"
|
||||||
|
) as mock_create_embed:
|
||||||
mock_create_embed.return_value = MagicMock()
|
mock_create_embed.return_value = MagicMock()
|
||||||
|
|
||||||
await view.remove_move_button.callback(mock_interaction)
|
await view.remove_move_button.callback(mock_interaction)
|
||||||
@ -104,7 +115,7 @@ class TestTransactionEmbedView:
|
|||||||
|
|
||||||
# Check that view is RemoveMoveView
|
# Check that view is RemoveMoveView
|
||||||
call_args = mock_interaction.response.edit_message.call_args
|
call_args = mock_interaction.response.edit_message.call_args
|
||||||
view_arg = call_args[1]['view']
|
view_arg = call_args[1]["view"]
|
||||||
assert isinstance(view_arg, RemoveMoveView)
|
assert isinstance(view_arg, RemoveMoveView)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -118,23 +129,33 @@ class TestTransactionEmbedView:
|
|||||||
mock_interaction.response.send_message.assert_called_once()
|
mock_interaction.response.send_message.assert_called_once()
|
||||||
call_args = mock_interaction.response.send_message.call_args
|
call_args = mock_interaction.response.send_message.call_args
|
||||||
assert "Cannot submit empty transaction" in call_args[0][0]
|
assert "Cannot submit empty transaction" in call_args[0][0]
|
||||||
assert call_args[1]['ephemeral'] is True
|
assert call_args[1]["ephemeral"] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_submit_button_illegal_transaction(self, mock_builder, mock_interaction):
|
async def test_submit_button_illegal_transaction(
|
||||||
|
self, mock_builder, mock_interaction
|
||||||
|
):
|
||||||
"""Test submit button with illegal transaction."""
|
"""Test submit button with illegal transaction."""
|
||||||
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
||||||
view.builder.is_empty = False
|
view.builder.is_empty = False
|
||||||
view.builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
|
view.builder.validate_transaction = AsyncMock(
|
||||||
is_legal=False,
|
return_value=RosterValidationResult(
|
||||||
major_league_count=26,
|
is_legal=False,
|
||||||
minor_league_count=10,
|
major_league_count=26,
|
||||||
warnings=[],
|
minor_league_count=10,
|
||||||
errors=["Too many players"],
|
warnings=[],
|
||||||
suggestions=["Drop 1 player"]
|
errors=["Too many players"],
|
||||||
))
|
suggestions=["Drop 1 player"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
await view.submit_button.callback(mock_interaction)
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
|
mock_current_state = MagicMock()
|
||||||
|
mock_current_state.week = 10
|
||||||
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
|
await view.submit_button.callback(mock_interaction)
|
||||||
|
|
||||||
mock_interaction.response.send_message.assert_called_once()
|
mock_interaction.response.send_message.assert_called_once()
|
||||||
call_args = mock_interaction.response.send_message.call_args
|
call_args = mock_interaction.response.send_message.call_args
|
||||||
@ -142,23 +163,33 @@ class TestTransactionEmbedView:
|
|||||||
assert "Cannot submit illegal transaction" in message
|
assert "Cannot submit illegal transaction" in message
|
||||||
assert "Too many players" in message
|
assert "Too many players" in message
|
||||||
assert "Drop 1 player" in message
|
assert "Drop 1 player" in message
|
||||||
assert call_args[1]['ephemeral'] is True
|
assert call_args[1]["ephemeral"] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_submit_button_legal_transaction(self, mock_builder, mock_interaction):
|
async def test_submit_button_legal_transaction(
|
||||||
|
self, mock_builder, mock_interaction
|
||||||
|
):
|
||||||
"""Test submit button with legal transaction."""
|
"""Test submit button with legal transaction."""
|
||||||
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
||||||
view.builder.is_empty = False
|
view.builder.is_empty = False
|
||||||
view.builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
|
view.builder.validate_transaction = AsyncMock(
|
||||||
is_legal=True,
|
return_value=RosterValidationResult(
|
||||||
major_league_count=25,
|
is_legal=True,
|
||||||
minor_league_count=10,
|
major_league_count=25,
|
||||||
warnings=[],
|
minor_league_count=10,
|
||||||
errors=[],
|
warnings=[],
|
||||||
suggestions=[]
|
errors=[],
|
||||||
))
|
suggestions=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
await view.submit_button.callback(mock_interaction)
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
|
mock_current_state = MagicMock()
|
||||||
|
mock_current_state.week = 10
|
||||||
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
|
await view.submit_button.callback(mock_interaction)
|
||||||
|
|
||||||
# Should send confirmation modal
|
# Should send confirmation modal
|
||||||
mock_interaction.response.send_modal.assert_called_once()
|
mock_interaction.response.send_modal.assert_called_once()
|
||||||
@ -169,7 +200,9 @@ class TestTransactionEmbedView:
|
|||||||
async def test_cancel_button(self, mock_builder, mock_interaction):
|
async def test_cancel_button(self, mock_builder, mock_interaction):
|
||||||
"""Test cancel button clears moves and disables view."""
|
"""Test cancel button clears moves and disables view."""
|
||||||
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
view = TransactionEmbedView(mock_builder, user_id=123456789)
|
||||||
with patch('views.transaction_embed.create_transaction_embed') as mock_create_embed:
|
with patch(
|
||||||
|
"views.transaction_embed.create_transaction_embed"
|
||||||
|
) as mock_create_embed:
|
||||||
mock_create_embed.return_value = MagicMock()
|
mock_create_embed.return_value = MagicMock()
|
||||||
|
|
||||||
await view.cancel_button.callback(mock_interaction)
|
await view.cancel_button.callback(mock_interaction)
|
||||||
@ -180,9 +213,7 @@ class TestTransactionEmbedView:
|
|||||||
# Should edit message with disabled view
|
# Should edit message with disabled view
|
||||||
mock_interaction.response.edit_message.assert_called_once()
|
mock_interaction.response.edit_message.assert_called_once()
|
||||||
call_args = mock_interaction.response.edit_message.call_args
|
call_args = mock_interaction.response.edit_message.call_args
|
||||||
assert "Transaction cancelled" in call_args[1]['content']
|
assert "Transaction cancelled" in call_args[1]["content"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestSubmitConfirmationModal:
|
class TestSubmitConfirmationModal:
|
||||||
@ -191,7 +222,13 @@ class TestSubmitConfirmationModal:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_builder(self):
|
def mock_builder(self):
|
||||||
"""Create mock TransactionBuilder."""
|
"""Create mock TransactionBuilder."""
|
||||||
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
|
team = Team(
|
||||||
|
id=499,
|
||||||
|
abbrev="WV",
|
||||||
|
sname="Black Bears",
|
||||||
|
lname="West Virginia Black Bears",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
builder = MagicMock(spec=TransactionBuilder)
|
builder = MagicMock(spec=TransactionBuilder)
|
||||||
builder.team = team
|
builder.team = team
|
||||||
builder.moves = []
|
builder.moves = []
|
||||||
@ -220,64 +257,86 @@ class TestSubmitConfirmationModal:
|
|||||||
mock_message.embeds[0].title = "📋 Transaction Builder"
|
mock_message.embeds[0].title = "📋 Transaction Builder"
|
||||||
mock_message.edit = AsyncMock()
|
mock_message.edit = AsyncMock()
|
||||||
|
|
||||||
interaction.channel.history.return_value.__aiter__ = AsyncMock(return_value=iter([mock_message]))
|
interaction.channel.history.return_value.__aiter__ = AsyncMock(
|
||||||
|
return_value=iter([mock_message])
|
||||||
|
)
|
||||||
|
|
||||||
return interaction
|
return interaction
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_modal_submit_wrong_confirmation(self, mock_builder, mock_interaction):
|
async def test_modal_submit_wrong_confirmation(
|
||||||
|
self, mock_builder, mock_interaction
|
||||||
|
):
|
||||||
"""Test modal submission with wrong confirmation text."""
|
"""Test modal submission with wrong confirmation text."""
|
||||||
modal = SubmitConfirmationModal(mock_builder)
|
modal = SubmitConfirmationModal(mock_builder)
|
||||||
# Mock the TextInput values
|
# Mock the TextInput values
|
||||||
modal.confirmation = MagicMock()
|
modal.confirmation = MagicMock()
|
||||||
modal.confirmation.value = 'WRONG'
|
modal.confirmation.value = "WRONG"
|
||||||
|
|
||||||
await modal.on_submit(mock_interaction)
|
await modal.on_submit(mock_interaction)
|
||||||
|
|
||||||
mock_interaction.response.send_message.assert_called_once()
|
mock_interaction.response.send_message.assert_called_once()
|
||||||
call_args = mock_interaction.response.send_message.call_args
|
call_args = mock_interaction.response.send_message.call_args
|
||||||
assert "must type 'CONFIRM' exactly" in call_args[0][0]
|
assert "must type 'CONFIRM' exactly" in call_args[0][0]
|
||||||
assert call_args[1]['ephemeral'] is True
|
assert call_args[1]["ephemeral"] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_modal_submit_correct_confirmation(self, mock_builder, mock_interaction):
|
async def test_modal_submit_correct_confirmation(
|
||||||
|
self, mock_builder, mock_interaction
|
||||||
|
):
|
||||||
"""Test modal submission with correct confirmation."""
|
"""Test modal submission with correct confirmation."""
|
||||||
modal = SubmitConfirmationModal(mock_builder)
|
modal = SubmitConfirmationModal(mock_builder)
|
||||||
# Mock the TextInput values
|
# Mock the TextInput values
|
||||||
modal.confirmation = MagicMock()
|
modal.confirmation = MagicMock()
|
||||||
modal.confirmation.value = 'CONFIRM'
|
modal.confirmation.value = "CONFIRM"
|
||||||
|
|
||||||
mock_transaction = MagicMock()
|
mock_transaction = MagicMock()
|
||||||
mock_transaction.moveid = 'Season-012-Week-11-123456789'
|
mock_transaction.moveid = "Season-012-Week-11-123456789"
|
||||||
mock_transaction.week = 11
|
mock_transaction.week = 11
|
||||||
mock_transaction.frozen = False # Will be set to True
|
mock_transaction.frozen = False # Will be set to True
|
||||||
|
|
||||||
with patch('services.league_service.league_service') as mock_league_service:
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
mock_current_state = MagicMock()
|
mock_current_state = MagicMock()
|
||||||
mock_current_state.week = 10
|
mock_current_state.week = 10
|
||||||
mock_current_state.freeze = True # Simulate Monday-Friday freeze period
|
mock_current_state.freeze = True # Simulate Monday-Friday freeze period
|
||||||
mock_league_service.get_current_state = AsyncMock(return_value=mock_current_state)
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
|
|
||||||
modal.builder.submit_transaction = AsyncMock(return_value=[mock_transaction])
|
modal.builder.submit_transaction = AsyncMock(
|
||||||
|
return_value=[mock_transaction]
|
||||||
|
)
|
||||||
|
|
||||||
with patch('services.transaction_service.transaction_service') as mock_transaction_service:
|
with patch(
|
||||||
|
"views.transaction_embed.transaction_service"
|
||||||
|
) as mock_transaction_service:
|
||||||
# Mock the create_transaction_batch call
|
# Mock the create_transaction_batch call
|
||||||
mock_transaction_service.create_transaction_batch = AsyncMock(return_value=[mock_transaction])
|
mock_transaction_service.create_transaction_batch = AsyncMock(
|
||||||
|
return_value=[mock_transaction]
|
||||||
|
)
|
||||||
|
|
||||||
with patch('utils.transaction_logging.post_transaction_to_log') as mock_post_log:
|
with patch(
|
||||||
|
"utils.transaction_logging.post_transaction_to_log"
|
||||||
|
) as mock_post_log:
|
||||||
mock_post_log.return_value = AsyncMock()
|
mock_post_log.return_value = AsyncMock()
|
||||||
|
|
||||||
with patch('services.transaction_builder.clear_transaction_builder') as mock_clear:
|
with patch(
|
||||||
|
"views.transaction_embed.clear_transaction_builder"
|
||||||
|
) as mock_clear:
|
||||||
await modal.on_submit(mock_interaction)
|
await modal.on_submit(mock_interaction)
|
||||||
|
|
||||||
# Should defer response
|
# Should defer response
|
||||||
mock_interaction.response.defer.assert_called_once_with(ephemeral=True)
|
mock_interaction.response.defer.assert_called_once_with(
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
# Should get current state
|
# Should get current state
|
||||||
mock_league_service.get_current_state.assert_called_once()
|
mock_league_service.get_current_state.assert_called_once()
|
||||||
|
|
||||||
# Should submit transaction for next week
|
# Should submit transaction for next week
|
||||||
modal.builder.submit_transaction.assert_called_once_with(week=11)
|
modal.builder.submit_transaction.assert_called_once_with(
|
||||||
|
week=11
|
||||||
|
)
|
||||||
|
|
||||||
# Should mark transaction as frozen (based on current_state.freeze)
|
# Should mark transaction as frozen (based on current_state.freeze)
|
||||||
assert mock_transaction.frozen is True
|
assert mock_transaction.frozen is True
|
||||||
@ -295,7 +354,9 @@ class TestSubmitConfirmationModal:
|
|||||||
assert mock_transaction.moveid in call_args[0][0]
|
assert mock_transaction.moveid in call_args[0][0]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_modal_submit_during_thaw_period(self, mock_builder, mock_interaction):
|
async def test_modal_submit_during_thaw_period(
|
||||||
|
self, mock_builder, mock_interaction
|
||||||
|
):
|
||||||
"""Test modal submission during thaw period (Saturday-Sunday) sets frozen=False.
|
"""Test modal submission during thaw period (Saturday-Sunday) sets frozen=False.
|
||||||
|
|
||||||
When freeze=False (Saturday-Sunday), transactions should NOT be frozen
|
When freeze=False (Saturday-Sunday), transactions should NOT be frozen
|
||||||
@ -305,29 +366,41 @@ class TestSubmitConfirmationModal:
|
|||||||
modal = SubmitConfirmationModal(mock_builder)
|
modal = SubmitConfirmationModal(mock_builder)
|
||||||
# Mock the TextInput values
|
# Mock the TextInput values
|
||||||
modal.confirmation = MagicMock()
|
modal.confirmation = MagicMock()
|
||||||
modal.confirmation.value = 'CONFIRM'
|
modal.confirmation.value = "CONFIRM"
|
||||||
|
|
||||||
mock_transaction = MagicMock()
|
mock_transaction = MagicMock()
|
||||||
mock_transaction.moveid = 'Season-012-Week-11-123456789'
|
mock_transaction.moveid = "Season-012-Week-11-123456789"
|
||||||
mock_transaction.week = 11
|
mock_transaction.week = 11
|
||||||
mock_transaction.frozen = True # Will be set to False during thaw
|
mock_transaction.frozen = True # Will be set to False during thaw
|
||||||
|
|
||||||
with patch('services.league_service.league_service') as mock_league_service:
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
mock_current_state = MagicMock()
|
mock_current_state = MagicMock()
|
||||||
mock_current_state.week = 10
|
mock_current_state.week = 10
|
||||||
mock_current_state.freeze = False # Simulate Saturday-Sunday thaw period
|
mock_current_state.freeze = False # Simulate Saturday-Sunday thaw period
|
||||||
mock_league_service.get_current_state = AsyncMock(return_value=mock_current_state)
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
|
|
||||||
modal.builder.submit_transaction = AsyncMock(return_value=[mock_transaction])
|
modal.builder.submit_transaction = AsyncMock(
|
||||||
|
return_value=[mock_transaction]
|
||||||
|
)
|
||||||
|
|
||||||
with patch('services.transaction_service.transaction_service') as mock_transaction_service:
|
with patch(
|
||||||
|
"views.transaction_embed.transaction_service"
|
||||||
|
) as mock_transaction_service:
|
||||||
# Mock the create_transaction_batch call
|
# Mock the create_transaction_batch call
|
||||||
mock_transaction_service.create_transaction_batch = AsyncMock(return_value=[mock_transaction])
|
mock_transaction_service.create_transaction_batch = AsyncMock(
|
||||||
|
return_value=[mock_transaction]
|
||||||
|
)
|
||||||
|
|
||||||
with patch('utils.transaction_logging.post_transaction_to_log') as mock_post_log:
|
with patch(
|
||||||
|
"utils.transaction_logging.post_transaction_to_log"
|
||||||
|
) as mock_post_log:
|
||||||
mock_post_log.return_value = AsyncMock()
|
mock_post_log.return_value = AsyncMock()
|
||||||
|
|
||||||
with patch('services.transaction_builder.clear_transaction_builder') as mock_clear:
|
with patch(
|
||||||
|
"views.transaction_embed.clear_transaction_builder"
|
||||||
|
) as mock_clear:
|
||||||
await modal.on_submit(mock_interaction)
|
await modal.on_submit(mock_interaction)
|
||||||
|
|
||||||
# Should mark transaction as NOT frozen (thaw period)
|
# Should mark transaction as NOT frozen (thaw period)
|
||||||
@ -339,9 +412,9 @@ class TestSubmitConfirmationModal:
|
|||||||
modal = SubmitConfirmationModal(mock_builder)
|
modal = SubmitConfirmationModal(mock_builder)
|
||||||
# Mock the TextInput values
|
# Mock the TextInput values
|
||||||
modal.confirmation = MagicMock()
|
modal.confirmation = MagicMock()
|
||||||
modal.confirmation.value = 'CONFIRM'
|
modal.confirmation.value = "CONFIRM"
|
||||||
|
|
||||||
with patch('services.league_service.league_service') as mock_league_service:
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
mock_league_service.get_current_state = AsyncMock(return_value=None)
|
mock_league_service.get_current_state = AsyncMock(return_value=None)
|
||||||
|
|
||||||
await modal.on_submit(mock_interaction)
|
await modal.on_submit(mock_interaction)
|
||||||
@ -349,7 +422,7 @@ class TestSubmitConfirmationModal:
|
|||||||
mock_interaction.followup.send.assert_called_once()
|
mock_interaction.followup.send.assert_called_once()
|
||||||
call_args = mock_interaction.followup.send.call_args
|
call_args = mock_interaction.followup.send.call_args
|
||||||
assert "Could not get current league state" in call_args[0][0]
|
assert "Could not get current league state" in call_args[0][0]
|
||||||
assert call_args[1]['ephemeral'] is True
|
assert call_args[1]["ephemeral"] is True
|
||||||
|
|
||||||
|
|
||||||
class TestEmbedCreation:
|
class TestEmbedCreation:
|
||||||
@ -358,7 +431,13 @@ class TestEmbedCreation:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_builder_empty(self):
|
def mock_builder_empty(self):
|
||||||
"""Create empty mock TransactionBuilder."""
|
"""Create empty mock TransactionBuilder."""
|
||||||
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
|
team = Team(
|
||||||
|
id=499,
|
||||||
|
abbrev="WV",
|
||||||
|
sname="Black Bears",
|
||||||
|
lname="West Virginia Black Bears",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
builder = MagicMock(spec=TransactionBuilder)
|
builder = MagicMock(spec=TransactionBuilder)
|
||||||
builder.team = team
|
builder.team = team
|
||||||
builder.is_empty = True
|
builder.is_empty = True
|
||||||
@ -366,20 +445,28 @@ class TestEmbedCreation:
|
|||||||
builder.moves = []
|
builder.moves = []
|
||||||
builder.created_at = MagicMock()
|
builder.created_at = MagicMock()
|
||||||
builder.created_at.strftime.return_value = "10:30:15"
|
builder.created_at.strftime.return_value = "10:30:15"
|
||||||
builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
|
builder.validate_transaction = AsyncMock(
|
||||||
is_legal=True,
|
return_value=RosterValidationResult(
|
||||||
major_league_count=24,
|
is_legal=True,
|
||||||
minor_league_count=10,
|
major_league_count=24,
|
||||||
warnings=[],
|
minor_league_count=10,
|
||||||
errors=[],
|
warnings=[],
|
||||||
suggestions=["Add player moves to build your transaction"]
|
errors=[],
|
||||||
))
|
suggestions=["Add player moves to build your transaction"],
|
||||||
|
)
|
||||||
|
)
|
||||||
return builder
|
return builder
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_builder_with_moves(self):
|
def mock_builder_with_moves(self):
|
||||||
"""Create mock TransactionBuilder with moves."""
|
"""Create mock TransactionBuilder with moves."""
|
||||||
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
|
team = Team(
|
||||||
|
id=499,
|
||||||
|
abbrev="WV",
|
||||||
|
sname="Black Bears",
|
||||||
|
lname="West Virginia Black Bears",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
builder = MagicMock(spec=TransactionBuilder)
|
builder = MagicMock(spec=TransactionBuilder)
|
||||||
builder.team = team
|
builder.team = team
|
||||||
builder.is_empty = False
|
builder.is_empty = False
|
||||||
@ -394,20 +481,28 @@ class TestEmbedCreation:
|
|||||||
|
|
||||||
builder.created_at = MagicMock()
|
builder.created_at = MagicMock()
|
||||||
builder.created_at.strftime.return_value = "10:30:15"
|
builder.created_at.strftime.return_value = "10:30:15"
|
||||||
builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
|
builder.validate_transaction = AsyncMock(
|
||||||
is_legal=False,
|
return_value=RosterValidationResult(
|
||||||
major_league_count=26,
|
is_legal=False,
|
||||||
minor_league_count=10,
|
major_league_count=26,
|
||||||
warnings=["Warning message"],
|
minor_league_count=10,
|
||||||
errors=["Error message"],
|
warnings=["Warning message"],
|
||||||
suggestions=["Suggestion message"]
|
errors=["Error message"],
|
||||||
))
|
suggestions=["Suggestion message"],
|
||||||
|
)
|
||||||
|
)
|
||||||
return builder
|
return builder
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_transaction_embed_empty(self, mock_builder_empty):
|
async def test_create_transaction_embed_empty(self, mock_builder_empty):
|
||||||
"""Test creating embed for empty transaction."""
|
"""Test creating embed for empty transaction."""
|
||||||
embed = await create_transaction_embed(mock_builder_empty)
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
|
mock_current_state = MagicMock()
|
||||||
|
mock_current_state.week = 10
|
||||||
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
|
embed = await create_transaction_embed(mock_builder_empty)
|
||||||
|
|
||||||
assert isinstance(embed, discord.Embed)
|
assert isinstance(embed, discord.Embed)
|
||||||
assert "Transaction Builder - WV" in embed.title
|
assert "Transaction Builder - WV" in embed.title
|
||||||
@ -420,19 +515,29 @@ class TestEmbedCreation:
|
|||||||
assert "Suggestions" in field_names
|
assert "Suggestions" in field_names
|
||||||
|
|
||||||
# Check empty moves message
|
# Check empty moves message
|
||||||
moves_field = next(field for field in embed.fields if field.name == "Current Moves")
|
moves_field = next(
|
||||||
|
field for field in embed.fields if field.name == "Current Moves"
|
||||||
|
)
|
||||||
assert "No moves yet" in moves_field.value
|
assert "No moves yet" in moves_field.value
|
||||||
|
|
||||||
# Check for Add More Moves instruction field
|
# Check for Add More Moves instruction field
|
||||||
field_names = [field.name for field in embed.fields]
|
field_names = [field.name for field in embed.fields]
|
||||||
assert "➕ Add More Moves" in field_names
|
assert "➕ Add More Moves" in field_names
|
||||||
add_moves_field = next(field for field in embed.fields if field.name == "➕ Add More Moves")
|
add_moves_field = next(
|
||||||
|
field for field in embed.fields if field.name == "➕ Add More Moves"
|
||||||
|
)
|
||||||
assert "/dropadd" in add_moves_field.value
|
assert "/dropadd" in add_moves_field.value
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_transaction_embed_with_moves(self, mock_builder_with_moves):
|
async def test_create_transaction_embed_with_moves(self, mock_builder_with_moves):
|
||||||
"""Test creating embed for transaction with moves."""
|
"""Test creating embed for transaction with moves."""
|
||||||
embed = await create_transaction_embed(mock_builder_with_moves)
|
with patch("views.transaction_embed.league_service") as mock_league_service:
|
||||||
|
mock_current_state = MagicMock()
|
||||||
|
mock_current_state.week = 10
|
||||||
|
mock_league_service.get_current_state = AsyncMock(
|
||||||
|
return_value=mock_current_state
|
||||||
|
)
|
||||||
|
embed = await create_transaction_embed(mock_builder_with_moves)
|
||||||
|
|
||||||
assert isinstance(embed, discord.Embed)
|
assert isinstance(embed, discord.Embed)
|
||||||
assert "Transaction Builder - WV" in embed.title
|
assert "Transaction Builder - WV" in embed.title
|
||||||
@ -445,13 +550,16 @@ class TestEmbedCreation:
|
|||||||
assert "Suggestions" in field_names
|
assert "Suggestions" in field_names
|
||||||
|
|
||||||
# Check moves content
|
# Check moves content
|
||||||
moves_field = next(field for field in embed.fields if "Current Moves" in field.name)
|
moves_field = next(
|
||||||
|
field for field in embed.fields if "Current Moves" in field.name
|
||||||
|
)
|
||||||
assert "Move 1: Player → Team" in moves_field.value
|
assert "Move 1: Player → Team" in moves_field.value
|
||||||
assert "Move 2: Player → Team" in moves_field.value
|
assert "Move 2: Player → Team" in moves_field.value
|
||||||
|
|
||||||
# Check for Add More Moves instruction field
|
# Check for Add More Moves instruction field
|
||||||
field_names = [field.name for field in embed.fields]
|
field_names = [field.name for field in embed.fields]
|
||||||
assert "➕ Add More Moves" in field_names
|
assert "➕ Add More Moves" in field_names
|
||||||
add_moves_field = next(field for field in embed.fields if field.name == "➕ Add More Moves")
|
add_moves_field = next(
|
||||||
|
field for field in embed.fields if field.name == "➕ Add More Moves"
|
||||||
|
)
|
||||||
assert "/dropadd" in add_moves_field.value
|
assert "/dropadd" in add_moves_field.value
|
||||||
|
|
||||||
@ -3,11 +3,19 @@ Interactive Transaction Embed Views
|
|||||||
|
|
||||||
Handles the Discord embed and button interfaces for the transaction builder.
|
Handles the Discord embed and button interfaces for the transaction builder.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from services.transaction_builder import TransactionBuilder, RosterValidationResult
|
from services.transaction_builder import (
|
||||||
|
TransactionBuilder,
|
||||||
|
RosterValidationResult,
|
||||||
|
clear_transaction_builder,
|
||||||
|
)
|
||||||
|
from services.league_service import league_service
|
||||||
|
from services.transaction_service import transaction_service
|
||||||
|
from services.player_service import player_service
|
||||||
from views.embeds import EmbedColors, EmbedTemplate
|
from views.embeds import EmbedColors, EmbedTemplate
|
||||||
from utils.transaction_logging import post_transaction_to_log
|
from utils.transaction_logging import post_transaction_to_log
|
||||||
|
|
||||||
@ -15,7 +23,13 @@ from utils.transaction_logging import post_transaction_to_log
|
|||||||
class TransactionEmbedView(discord.ui.View):
|
class TransactionEmbedView(discord.ui.View):
|
||||||
"""Interactive view for the transaction builder embed."""
|
"""Interactive view for the transaction builder embed."""
|
||||||
|
|
||||||
def __init__(self, builder: TransactionBuilder, user_id: int, submission_handler: str = "scheduled", command_name: str = "/dropadd"):
|
def __init__(
|
||||||
|
self,
|
||||||
|
builder: TransactionBuilder,
|
||||||
|
user_id: int,
|
||||||
|
submission_handler: str = "scheduled",
|
||||||
|
command_name: str = "/dropadd",
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the transaction embed view.
|
Initialize the transaction embed view.
|
||||||
|
|
||||||
@ -36,7 +50,7 @@ class TransactionEmbedView(discord.ui.View):
|
|||||||
if interaction.user.id != self.user_id:
|
if interaction.user.id != self.user_id:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"❌ You don't have permission to use this transaction builder.",
|
"❌ You don't have permission to use this transaction builder.",
|
||||||
ephemeral=True
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -49,12 +63,13 @@ class TransactionEmbedView(discord.ui.View):
|
|||||||
item.disabled = True
|
item.disabled = True
|
||||||
|
|
||||||
@discord.ui.button(label="Remove Move", style=discord.ButtonStyle.red, emoji="➖")
|
@discord.ui.button(label="Remove Move", style=discord.ButtonStyle.red, emoji="➖")
|
||||||
async def remove_move_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def remove_move_button(
|
||||||
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
|
):
|
||||||
"""Handle remove move button click."""
|
"""Handle remove move button click."""
|
||||||
if self.builder.is_empty:
|
if self.builder.is_empty:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"❌ No moves to remove. Add some moves first!",
|
"❌ No moves to remove. Add some moves first!", ephemeral=True
|
||||||
ephemeral=True
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -64,25 +79,36 @@ class TransactionEmbedView(discord.ui.View):
|
|||||||
|
|
||||||
await interaction.response.edit_message(embed=embed, view=select_view)
|
await interaction.response.edit_message(embed=embed, view=select_view)
|
||||||
|
|
||||||
@discord.ui.button(label="Submit Transaction", style=discord.ButtonStyle.primary, emoji="📤")
|
@discord.ui.button(
|
||||||
async def submit_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
label="Submit Transaction", style=discord.ButtonStyle.primary, emoji="📤"
|
||||||
|
)
|
||||||
|
async def submit_button(
|
||||||
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
|
):
|
||||||
"""Handle submit transaction button click."""
|
"""Handle submit transaction button click."""
|
||||||
if self.builder.is_empty:
|
if self.builder.is_empty:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"❌ Cannot submit empty transaction. Add some moves first!",
|
"❌ Cannot submit empty transaction. Add some moves first!",
|
||||||
ephemeral=True
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Validate before submission
|
# Validate before submission (include pre-existing transactions for scheduled moves)
|
||||||
validation = await self.builder.validate_transaction()
|
if self.submission_handler == "scheduled":
|
||||||
|
current_state = await league_service.get_current_state()
|
||||||
|
next_week = current_state.week + 1 if current_state else None
|
||||||
|
validation = await self.builder.validate_transaction(next_week=next_week)
|
||||||
|
else:
|
||||||
|
validation = await self.builder.validate_transaction()
|
||||||
if not validation.is_legal:
|
if not validation.is_legal:
|
||||||
error_msg = "❌ **Cannot submit illegal transaction:**\n"
|
error_msg = "❌ **Cannot submit illegal transaction:**\n"
|
||||||
error_msg += "\n".join([f"• {error}" for error in validation.errors])
|
error_msg += "\n".join([f"• {error}" for error in validation.errors])
|
||||||
|
|
||||||
if validation.suggestions:
|
if validation.suggestions:
|
||||||
error_msg += "\n\n**Suggestions:**\n"
|
error_msg += "\n\n**Suggestions:**\n"
|
||||||
error_msg += "\n".join([f"💡 {suggestion}" for suggestion in validation.suggestions])
|
error_msg += "\n".join(
|
||||||
|
[f"💡 {suggestion}" for suggestion in validation.suggestions]
|
||||||
|
)
|
||||||
|
|
||||||
await interaction.response.send_message(error_msg, ephemeral=True)
|
await interaction.response.send_message(error_msg, ephemeral=True)
|
||||||
return
|
return
|
||||||
@ -92,7 +118,9 @@ class TransactionEmbedView(discord.ui.View):
|
|||||||
await interaction.response.send_modal(modal)
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary, emoji="❌")
|
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary, emoji="❌")
|
||||||
async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def cancel_button(
|
||||||
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
|
):
|
||||||
"""Handle cancel button click."""
|
"""Handle cancel button click."""
|
||||||
self.builder.clear_moves()
|
self.builder.clear_moves()
|
||||||
embed = await create_transaction_embed(self.builder, self.command_name)
|
embed = await create_transaction_embed(self.builder, self.command_name)
|
||||||
@ -103,9 +131,7 @@ class TransactionEmbedView(discord.ui.View):
|
|||||||
item.disabled = True
|
item.disabled = True
|
||||||
|
|
||||||
await interaction.response.edit_message(
|
await interaction.response.edit_message(
|
||||||
content="❌ **Transaction cancelled and cleared.**",
|
content="❌ **Transaction cancelled and cleared.**", embed=embed, view=self
|
||||||
embed=embed,
|
|
||||||
view=self
|
|
||||||
)
|
)
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
@ -113,7 +139,9 @@ class TransactionEmbedView(discord.ui.View):
|
|||||||
class RemoveMoveView(discord.ui.View):
|
class RemoveMoveView(discord.ui.View):
|
||||||
"""View for selecting which move to remove."""
|
"""View for selecting which move to remove."""
|
||||||
|
|
||||||
def __init__(self, builder: TransactionBuilder, user_id: int, command_name: str = "/dropadd"):
|
def __init__(
|
||||||
|
self, builder: TransactionBuilder, user_id: int, command_name: str = "/dropadd"
|
||||||
|
):
|
||||||
super().__init__(timeout=300.0) # 5 minute timeout
|
super().__init__(timeout=300.0) # 5 minute timeout
|
||||||
self.builder = builder
|
self.builder = builder
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
@ -124,15 +152,21 @@ class RemoveMoveView(discord.ui.View):
|
|||||||
self.add_item(RemoveMoveSelect(builder, command_name))
|
self.add_item(RemoveMoveSelect(builder, command_name))
|
||||||
|
|
||||||
# Add back button
|
# Add back button
|
||||||
back_button = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, emoji="⬅️")
|
back_button = discord.ui.Button(
|
||||||
|
label="Back", style=discord.ButtonStyle.secondary, emoji="⬅️"
|
||||||
|
)
|
||||||
back_button.callback = self.back_callback
|
back_button.callback = self.back_callback
|
||||||
self.add_item(back_button)
|
self.add_item(back_button)
|
||||||
|
|
||||||
async def back_callback(self, interaction: discord.Interaction):
|
async def back_callback(self, interaction: discord.Interaction):
|
||||||
"""Handle back button to return to main view."""
|
"""Handle back button to return to main view."""
|
||||||
# Determine submission_handler from command_name
|
# Determine submission_handler from command_name
|
||||||
submission_handler = "immediate" if self.command_name == "/ilmove" else "scheduled"
|
submission_handler = (
|
||||||
main_view = TransactionEmbedView(self.builder, self.user_id, submission_handler, self.command_name)
|
"immediate" if self.command_name == "/ilmove" else "scheduled"
|
||||||
|
)
|
||||||
|
main_view = TransactionEmbedView(
|
||||||
|
self.builder, self.user_id, submission_handler, self.command_name
|
||||||
|
)
|
||||||
embed = await create_transaction_embed(self.builder, self.command_name)
|
embed = await create_transaction_embed(self.builder, self.command_name)
|
||||||
await interaction.response.edit_message(embed=embed, view=main_view)
|
await interaction.response.edit_message(embed=embed, view=main_view)
|
||||||
|
|
||||||
@ -151,17 +185,19 @@ class RemoveMoveSelect(discord.ui.Select):
|
|||||||
# Create options from current moves
|
# Create options from current moves
|
||||||
options = []
|
options = []
|
||||||
for i, move in enumerate(builder.moves[:25]): # Discord limit of 25 options
|
for i, move in enumerate(builder.moves[:25]): # Discord limit of 25 options
|
||||||
options.append(discord.SelectOption(
|
options.append(
|
||||||
label=f"{move.player.name}",
|
discord.SelectOption(
|
||||||
description=move.description[:100], # Discord description limit
|
label=f"{move.player.name}",
|
||||||
value=str(move.player.id)
|
description=move.description[:100], # Discord description limit
|
||||||
))
|
value=str(move.player.id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
placeholder="Select a move to remove...",
|
placeholder="Select a move to remove...",
|
||||||
min_values=1,
|
min_values=1,
|
||||||
max_values=1,
|
max_values=1,
|
||||||
options=options
|
options=options,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def callback(self, interaction: discord.Interaction):
|
async def callback(self, interaction: discord.Interaction):
|
||||||
@ -172,30 +208,33 @@ class RemoveMoveSelect(discord.ui.Select):
|
|||||||
if move:
|
if move:
|
||||||
self.builder.remove_move(player_id)
|
self.builder.remove_move(player_id)
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"✅ Removed: {move.description}",
|
f"✅ Removed: {move.description}", ephemeral=True
|
||||||
ephemeral=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update the embed
|
# Update the embed
|
||||||
# Determine submission_handler from command_name
|
# Determine submission_handler from command_name
|
||||||
submission_handler = "immediate" if self.command_name == "/ilmove" else "scheduled"
|
submission_handler = (
|
||||||
main_view = TransactionEmbedView(self.builder, interaction.user.id, submission_handler, self.command_name)
|
"immediate" if self.command_name == "/ilmove" else "scheduled"
|
||||||
|
)
|
||||||
|
main_view = TransactionEmbedView(
|
||||||
|
self.builder, interaction.user.id, submission_handler, self.command_name
|
||||||
|
)
|
||||||
embed = await create_transaction_embed(self.builder, self.command_name)
|
embed = await create_transaction_embed(self.builder, self.command_name)
|
||||||
|
|
||||||
# Edit the original message
|
# Edit the original message
|
||||||
await interaction.edit_original_response(embed=embed, view=main_view)
|
await interaction.edit_original_response(embed=embed, view=main_view)
|
||||||
else:
|
else:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"❌ Could not find that move to remove.",
|
"❌ Could not find that move to remove.", ephemeral=True
|
||||||
ephemeral=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SubmitConfirmationModal(discord.ui.Modal):
|
class SubmitConfirmationModal(discord.ui.Modal):
|
||||||
"""Modal for confirming transaction submission."""
|
"""Modal for confirming transaction submission."""
|
||||||
|
|
||||||
def __init__(self, builder: TransactionBuilder, submission_handler: str = "scheduled"):
|
def __init__(
|
||||||
|
self, builder: TransactionBuilder, submission_handler: str = "scheduled"
|
||||||
|
):
|
||||||
super().__init__(title="Confirm Transaction Submission")
|
super().__init__(title="Confirm Transaction Submission")
|
||||||
self.builder = builder
|
self.builder = builder
|
||||||
self.submission_handler = submission_handler
|
self.submission_handler = submission_handler
|
||||||
@ -204,7 +243,7 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
label="Type 'CONFIRM' to submit",
|
label="Type 'CONFIRM' to submit",
|
||||||
placeholder="CONFIRM",
|
placeholder="CONFIRM",
|
||||||
required=True,
|
required=True,
|
||||||
max_length=7
|
max_length=7,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add_item(self.confirmation)
|
self.add_item(self.confirmation)
|
||||||
@ -214,31 +253,29 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
if self.confirmation.value.upper() != "CONFIRM":
|
if self.confirmation.value.upper() != "CONFIRM":
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"❌ Transaction not submitted. You must type 'CONFIRM' exactly.",
|
"❌ Transaction not submitted. You must type 'CONFIRM' exactly.",
|
||||||
ephemeral=True
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await interaction.response.defer(ephemeral=True)
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from services.league_service import league_service
|
|
||||||
from services.transaction_service import transaction_service
|
|
||||||
from services.player_service import player_service
|
|
||||||
|
|
||||||
# Get current league state
|
# Get current league state
|
||||||
current_state = await league_service.get_current_state()
|
current_state = await league_service.get_current_state()
|
||||||
|
|
||||||
if not current_state:
|
if not current_state:
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
"❌ Could not get current league state. Please try again later.",
|
"❌ Could not get current league state. Please try again later.",
|
||||||
ephemeral=True
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.submission_handler == "scheduled":
|
if self.submission_handler == "scheduled":
|
||||||
# SCHEDULED SUBMISSION (/dropadd behavior)
|
# SCHEDULED SUBMISSION (/dropadd behavior)
|
||||||
# Submit the transaction for NEXT week
|
# Submit the transaction for NEXT week
|
||||||
transactions = await self.builder.submit_transaction(week=current_state.week + 1)
|
transactions = await self.builder.submit_transaction(
|
||||||
|
week=current_state.week + 1
|
||||||
|
)
|
||||||
|
|
||||||
# Set frozen flag based on current freeze state:
|
# Set frozen flag based on current freeze state:
|
||||||
# - During freeze (Mon-Fri): frozen=True -> wait for Saturday conflict resolution
|
# - During freeze (Mon-Fri): frozen=True -> wait for Saturday conflict resolution
|
||||||
@ -247,13 +284,17 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
txn.frozen = current_state.freeze
|
txn.frozen = current_state.freeze
|
||||||
|
|
||||||
# POST transactions to database
|
# POST transactions to database
|
||||||
created_transactions = await transaction_service.create_transaction_batch(transactions)
|
created_transactions = (
|
||||||
|
await transaction_service.create_transaction_batch(transactions)
|
||||||
|
)
|
||||||
|
|
||||||
# Post to #transaction-log channel (only when league is NOT frozen)
|
# Post to #transaction-log channel (only when league is NOT frozen)
|
||||||
# During freeze period, transactions are hidden until Saturday processing
|
# During freeze period, transactions are hidden until Saturday processing
|
||||||
if not current_state.freeze:
|
if not current_state.freeze:
|
||||||
bot = interaction.client
|
bot = interaction.client
|
||||||
await post_transaction_to_log(bot, created_transactions, team=self.builder.team)
|
await post_transaction_to_log(
|
||||||
|
bot, created_transactions, team=self.builder.team
|
||||||
|
)
|
||||||
|
|
||||||
# Create success message
|
# Create success message
|
||||||
success_msg = f"✅ **Transaction Submitted Successfully!**\n\n"
|
success_msg = f"✅ **Transaction Submitted Successfully!**\n\n"
|
||||||
@ -277,27 +318,28 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
# Submit the transaction for THIS week
|
# Submit the transaction for THIS week
|
||||||
# Don't check existing transactions - they're already in DB and would cause double-counting
|
# Don't check existing transactions - they're already in DB and would cause double-counting
|
||||||
transactions = await self.builder.submit_transaction(
|
transactions = await self.builder.submit_transaction(
|
||||||
week=current_state.week,
|
week=current_state.week, check_existing_transactions=False
|
||||||
check_existing_transactions=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# POST transactions to database
|
# POST transactions to database
|
||||||
created_transactions = await transaction_service.create_transaction_batch(transactions)
|
created_transactions = (
|
||||||
|
await transaction_service.create_transaction_batch(transactions)
|
||||||
|
)
|
||||||
|
|
||||||
# Update each player's team assignment
|
# Update each player's team assignment
|
||||||
player_updates = []
|
player_updates = []
|
||||||
for txn in created_transactions:
|
for txn in created_transactions:
|
||||||
updated_player = await player_service.update_player_team(
|
updated_player = await player_service.update_player_team(
|
||||||
txn.player.id,
|
txn.player.id, txn.newteam.id, dem_week=current_state.week
|
||||||
txn.newteam.id,
|
|
||||||
dem_week=current_state.week
|
|
||||||
)
|
)
|
||||||
player_updates.append(updated_player)
|
player_updates.append(updated_player)
|
||||||
|
|
||||||
# Post to #transaction-log channel
|
# Post to #transaction-log channel
|
||||||
# IL moves always post immediately - they're intra-team and don't need freeze hiding
|
# IL moves always post immediately - they're intra-team and don't need freeze hiding
|
||||||
bot = interaction.client
|
bot = interaction.client
|
||||||
await post_transaction_to_log(bot, created_transactions, team=self.builder.team)
|
await post_transaction_to_log(
|
||||||
|
bot, created_transactions, team=self.builder.team
|
||||||
|
)
|
||||||
|
|
||||||
# Create success message
|
# Create success message
|
||||||
success_msg = f"✅ **IL Move Executed Successfully!**\n\n"
|
success_msg = f"✅ **IL Move Executed Successfully!**\n\n"
|
||||||
@ -314,15 +356,18 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
await interaction.followup.send(success_msg, ephemeral=True)
|
await interaction.followup.send(success_msg, ephemeral=True)
|
||||||
|
|
||||||
# Clear the builder after successful submission
|
# Clear the builder after successful submission
|
||||||
from services.transaction_builder import clear_transaction_builder
|
|
||||||
clear_transaction_builder(interaction.user.id)
|
clear_transaction_builder(interaction.user.id)
|
||||||
|
|
||||||
# Update the original embed to show completion
|
# Update the original embed to show completion
|
||||||
completion_title = "✅ Transaction Submitted" if self.submission_handler == "scheduled" else "✅ IL Move Executed"
|
completion_title = (
|
||||||
|
"✅ Transaction Submitted"
|
||||||
|
if self.submission_handler == "scheduled"
|
||||||
|
else "✅ IL Move Executed"
|
||||||
|
)
|
||||||
completion_embed = discord.Embed(
|
completion_embed = discord.Embed(
|
||||||
title=completion_title,
|
title=completion_title,
|
||||||
description=f"Your transaction has been processed successfully!",
|
description=f"Your transaction has been processed successfully!",
|
||||||
color=0x00ff00
|
color=0x00FF00,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Disable all buttons
|
# Disable all buttons
|
||||||
@ -330,9 +375,9 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Find and update the original message
|
# Find and update the original message
|
||||||
async for message in interaction.channel.history(limit=50): # type: ignore
|
async for message in interaction.channel.history(limit=50): # type: ignore
|
||||||
if message.author == interaction.client.user and message.embeds:
|
if message.author == interaction.client.user and message.embeds:
|
||||||
if "Transaction Builder" in message.embeds[0].title: # type: ignore
|
if "Transaction Builder" in message.embeds[0].title: # type: ignore
|
||||||
await message.edit(embed=completion_embed, view=view)
|
await message.edit(embed=completion_embed, view=view)
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
@ -340,12 +385,13 @@ class SubmitConfirmationModal(discord.ui.Modal):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
f"❌ Error submitting transaction: {str(e)}",
|
f"❌ Error submitting transaction: {str(e)}", ephemeral=True
|
||||||
ephemeral=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_transaction_embed(builder: TransactionBuilder, command_name: str = "/dropadd") -> discord.Embed:
|
async def create_transaction_embed(
|
||||||
|
builder: TransactionBuilder, command_name: str = "/dropadd"
|
||||||
|
) -> discord.Embed:
|
||||||
"""
|
"""
|
||||||
Create the main transaction builder embed.
|
Create the main transaction builder embed.
|
||||||
|
|
||||||
@ -365,7 +411,7 @@ async def create_transaction_embed(builder: TransactionBuilder, command_name: st
|
|||||||
embed = EmbedTemplate.create_base_embed(
|
embed = EmbedTemplate.create_base_embed(
|
||||||
title=f"📋 Transaction Builder - {builder.team.abbrev}",
|
title=f"📋 Transaction Builder - {builder.team.abbrev}",
|
||||||
description=description,
|
description=description,
|
||||||
color=EmbedColors.PRIMARY
|
color=EmbedColors.PRIMARY,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add current moves section
|
# Add current moves section
|
||||||
@ -373,7 +419,7 @@ async def create_transaction_embed(builder: TransactionBuilder, command_name: st
|
|||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Current Moves",
|
name="Current Moves",
|
||||||
value="*No moves yet. Use the buttons below to build your transaction.*",
|
value="*No moves yet. Use the buttons below to build your transaction.*",
|
||||||
inline=False
|
inline=False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
moves_text = ""
|
moves_text = ""
|
||||||
@ -384,65 +430,56 @@ async def create_transaction_embed(builder: TransactionBuilder, command_name: st
|
|||||||
moves_text += f"... and {len(builder.moves) - 10} more moves"
|
moves_text += f"... and {len(builder.moves) - 10} more moves"
|
||||||
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name=f"Current Moves ({builder.move_count})",
|
name=f"Current Moves ({builder.move_count})", value=moves_text, inline=False
|
||||||
value=moves_text,
|
|
||||||
inline=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add roster validation
|
# Add roster validation (include pre-existing transactions for scheduled moves)
|
||||||
validation = await builder.validate_transaction()
|
if command_name != "/ilmove":
|
||||||
|
current_state = await league_service.get_current_state()
|
||||||
|
next_week = current_state.week + 1 if current_state else None
|
||||||
|
validation = await builder.validate_transaction(next_week=next_week)
|
||||||
|
else:
|
||||||
|
validation = await builder.validate_transaction()
|
||||||
|
|
||||||
roster_status = f"{validation.major_league_status}\n{validation.minor_league_status}"
|
roster_status = (
|
||||||
|
f"{validation.major_league_status}\n{validation.minor_league_status}"
|
||||||
embed.add_field(
|
|
||||||
name="Roster Status",
|
|
||||||
value=roster_status,
|
|
||||||
inline=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
embed.add_field(name="Roster Status", value=roster_status, inline=False)
|
||||||
|
|
||||||
# Add sWAR status
|
# Add sWAR status
|
||||||
swar_status = f"{validation.major_league_swar_status}\n{validation.minor_league_swar_status}"
|
swar_status = (
|
||||||
embed.add_field(
|
f"{validation.major_league_swar_status}\n{validation.minor_league_swar_status}"
|
||||||
name="Team sWAR",
|
|
||||||
value=swar_status,
|
|
||||||
inline=False
|
|
||||||
)
|
)
|
||||||
|
embed.add_field(name="Team sWAR", value=swar_status, inline=False)
|
||||||
|
|
||||||
# Add pre-existing transactions note if applicable
|
# Add pre-existing transactions note if applicable
|
||||||
if validation.pre_existing_transactions_note:
|
if validation.pre_existing_transactions_note:
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="📋 Transaction Context",
|
name="📋 Transaction Context",
|
||||||
value=validation.pre_existing_transactions_note,
|
value=validation.pre_existing_transactions_note,
|
||||||
inline=False
|
inline=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add suggestions/errors
|
# Add suggestions/errors
|
||||||
if validation.errors:
|
if validation.errors:
|
||||||
error_text = "\n".join([f"• {error}" for error in validation.errors])
|
error_text = "\n".join([f"• {error}" for error in validation.errors])
|
||||||
embed.add_field(
|
embed.add_field(name="❌ Errors", value=error_text, inline=False)
|
||||||
name="❌ Errors",
|
|
||||||
value=error_text,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if validation.suggestions:
|
if validation.suggestions:
|
||||||
suggestion_text = "\n".join([f"💡 {suggestion}" for suggestion in validation.suggestions])
|
suggestion_text = "\n".join(
|
||||||
embed.add_field(
|
[f"💡 {suggestion}" for suggestion in validation.suggestions]
|
||||||
name="Suggestions",
|
|
||||||
value=suggestion_text,
|
|
||||||
inline=False
|
|
||||||
)
|
)
|
||||||
|
embed.add_field(name="Suggestions", value=suggestion_text, inline=False)
|
||||||
|
|
||||||
# Add instructions for adding more moves
|
# Add instructions for adding more moves
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="➕ Add More Moves",
|
name="➕ Add More Moves",
|
||||||
value=f"Use `{command_name}` to add more moves",
|
value=f"Use `{command_name}` to add more moves",
|
||||||
inline=False
|
inline=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add footer with timestamp
|
# Add footer with timestamp
|
||||||
embed.set_footer(text=f"Created at {builder.created_at.strftime('%H:%M:%S')}")
|
embed.set_footer(text=f"Created at {builder.created_at.strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user