Merge pull request 'fix: show validation errors in trade embed Quick Status' (#70) from fix/trade-embed-errors-and-cleanup into main
All checks were successful
Build Docker Image / build (push) Successful in 50s

Reviewed-on: #70
This commit is contained in:
cal 2026-03-08 16:29:06 +00:00
commit b3b8cd9683
2 changed files with 324 additions and 263 deletions

View File

@ -3,6 +3,7 @@ Trade Builder Service
Extends the TransactionBuilder to support multi-team trades and player exchanges. Extends the TransactionBuilder to support multi-team trades and player exchanges.
""" """
import logging import logging
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
from datetime import datetime, timezone from datetime import datetime, timezone
@ -12,10 +13,14 @@ from config import get_config
from models.trade import Trade, TradeMove, TradeStatus from models.trade import Trade, TradeMove, TradeStatus
from models.team import Team, RosterType from models.team import Team, RosterType
from models.player import Player from models.player import Player
from services.transaction_builder import TransactionBuilder, RosterValidationResult, TransactionMove from services.transaction_builder import (
TransactionBuilder,
RosterValidationResult,
TransactionMove,
)
from services.team_service import team_service from services.team_service import team_service
logger = logging.getLogger(f'{__name__}.TradeBuilder') logger = logging.getLogger(f"{__name__}.TradeBuilder")
class TradeValidationResult: class TradeValidationResult:
@ -52,7 +57,9 @@ class TradeValidationResult:
suggestions.extend(validation.suggestions) suggestions.extend(validation.suggestions)
return suggestions return suggestions
def get_participant_validation(self, team_id: int) -> Optional[RosterValidationResult]: def get_participant_validation(
self, team_id: int
) -> Optional[RosterValidationResult]:
"""Get validation result for a specific team.""" """Get validation result for a specific team."""
return self.participant_validations.get(team_id) return self.participant_validations.get(team_id)
@ -64,7 +71,12 @@ class TradeBuilder:
Extends the functionality of TransactionBuilder to support trades between teams. Extends the functionality of TransactionBuilder to support trades between teams.
""" """
def __init__(self, initiated_by: int, initiating_team: Team, season: int = get_config().sba_season): def __init__(
self,
initiated_by: int,
initiating_team: Team,
season: int = get_config().sba_season,
):
""" """
Initialize trade builder. Initialize trade builder.
@ -79,7 +91,7 @@ class TradeBuilder:
status=TradeStatus.DRAFT, status=TradeStatus.DRAFT,
initiated_by=initiated_by, initiated_by=initiated_by,
created_at=datetime.now(timezone.utc).isoformat(), created_at=datetime.now(timezone.utc).isoformat(),
season=season season=season,
) )
# Add the initiating team as first participant # Add the initiating team as first participant
@ -91,7 +103,9 @@ class TradeBuilder:
# Track which teams have accepted the trade (team_id -> True) # Track which teams have accepted the trade (team_id -> True)
self.accepted_teams: Set[int] = set() self.accepted_teams: Set[int] = set()
logger.info(f"TradeBuilder initialized: {self.trade.trade_id} by user {initiated_by} for {initiating_team.abbrev}") logger.info(
f"TradeBuilder initialized: {self.trade.trade_id} by user {initiated_by} for {initiating_team.abbrev}"
)
@property @property
def trade_id(self) -> str: def trade_id(self) -> str:
@ -127,7 +141,11 @@ class TradeBuilder:
@property @property
def pending_teams(self) -> List[Team]: def pending_teams(self) -> List[Team]:
"""Get list of teams that haven't accepted yet.""" """Get list of teams that haven't accepted yet."""
return [team for team in self.participating_teams if team.id not in self.accepted_teams] return [
team
for team in self.participating_teams
if team.id not in self.accepted_teams
]
def accept_trade(self, team_id: int) -> bool: def accept_trade(self, team_id: int) -> bool:
""" """
@ -140,7 +158,9 @@ class TradeBuilder:
True if all teams have now accepted, False otherwise True if all teams have now accepted, False otherwise
""" """
self.accepted_teams.add(team_id) self.accepted_teams.add(team_id)
logger.info(f"Team {team_id} accepted trade {self.trade_id}. Accepted: {len(self.accepted_teams)}/{self.team_count}") logger.info(
f"Team {team_id} accepted trade {self.trade_id}. Accepted: {len(self.accepted_teams)}/{self.team_count}"
)
return self.all_teams_accepted return self.all_teams_accepted
def reject_trade(self) -> None: def reject_trade(self) -> None:
@ -160,7 +180,9 @@ class TradeBuilder:
Returns: Returns:
Dict mapping team_id to acceptance status (True/False) Dict mapping team_id to acceptance status (True/False)
""" """
return {team.id: team.id in self.accepted_teams for team in self.participating_teams} return {
team.id: team.id in self.accepted_teams for team in self.participating_teams
}
def has_team_accepted(self, team_id: int) -> bool: def has_team_accepted(self, team_id: int) -> bool:
"""Check if a specific team has accepted.""" """Check if a specific team has accepted."""
@ -184,7 +206,9 @@ class TradeBuilder:
participant = self.trade.add_participant(team) participant = self.trade.add_participant(team)
# Create transaction builder for this team # Create transaction builder for this team
self._team_builders[team.id] = TransactionBuilder(team, self.trade.initiated_by, self.trade.season) self._team_builders[team.id] = TransactionBuilder(
team, self.trade.initiated_by, self.trade.season
)
# Register team in secondary index for multi-GM access # Register team in secondary index for multi-GM access
trade_key = f"{self.trade.initiated_by}:trade" trade_key = f"{self.trade.initiated_by}:trade"
@ -209,7 +233,10 @@ class TradeBuilder:
# Check if team has moves - prevent removal if they do # Check if team has moves - prevent removal if they do
if participant.all_moves: if participant.all_moves:
return False, f"{participant.team.abbrev} has moves in this trade and cannot be removed" return (
False,
f"{participant.team.abbrev} has moves in this trade and cannot be removed",
)
# Remove team # Remove team
removed = self.trade.remove_participant(team_id) removed = self.trade.remove_participant(team_id)
@ -229,7 +256,7 @@ class TradeBuilder:
from_team: Team, from_team: Team,
to_team: Team, to_team: Team,
from_roster: RosterType, from_roster: RosterType,
to_roster: RosterType to_roster: RosterType,
) -> tuple[bool, str]: ) -> tuple[bool, str]:
""" """
Add a player move to the trade. Add a player move to the trade.
@ -246,7 +273,10 @@ class TradeBuilder:
""" """
# Validate player is not from Free Agency # Validate player is not from Free Agency
if player.team_id == get_config().free_agent_team_id: if player.team_id == get_config().free_agent_team_id:
return False, f"Cannot add {player.name} from Free Agency. Players must be traded from teams within the organizations involved in the trade." return (
False,
f"Cannot add {player.name} from Free Agency. Players must be traded from teams within the organizations involved in the trade.",
)
# Validate player has a valid team assignment # Validate player has a valid team assignment
if not player.team_id: if not player.team_id:
@ -259,7 +289,10 @@ class TradeBuilder:
# Check if player's team is in the same organization as from_team # Check if player's team is in the same organization as from_team
if not player_team.is_same_organization(from_team): if not player_team.is_same_organization(from_team):
return False, f"{player.name} is on {player_team.abbrev}, they are not eligible to be added to the trade." return (
False,
f"{player.name} is on {player_team.abbrev}, they are not eligible to be added to the trade.",
)
# Ensure both teams are participating (check by organization for ML authority) # Ensure both teams are participating (check by organization for ML authority)
from_participant = self.trade.get_participant_by_organization(from_team) from_participant = self.trade.get_participant_by_organization(from_team)
@ -274,7 +307,10 @@ class TradeBuilder:
for participant in self.trade.participants: for participant in self.trade.participants:
for existing_move in participant.all_moves: for existing_move in participant.all_moves:
if existing_move.player.id == player.id: if existing_move.player.id == player.id:
return False, f"{player.name} is already involved in a move in this trade" return (
False,
f"{player.name} is already involved in a move in this trade",
)
# Create trade move # Create trade move
trade_move = TradeMove( trade_move = TradeMove(
@ -284,7 +320,7 @@ class TradeBuilder:
from_team=from_team, from_team=from_team,
to_team=to_team, to_team=to_team,
source_team=from_team, source_team=from_team,
destination_team=to_team destination_team=to_team,
) )
# Add to giving team's moves # Add to giving team's moves
@ -303,7 +339,7 @@ class TradeBuilder:
from_roster=from_roster, from_roster=from_roster,
to_roster=RosterType.FREE_AGENCY, # Conceptually leaving the org to_roster=RosterType.FREE_AGENCY, # Conceptually leaving the org
from_team=from_team, from_team=from_team,
to_team=None to_team=None,
) )
# Move for receiving team (player joining) # Move for receiving team (player joining)
@ -312,19 +348,23 @@ class TradeBuilder:
from_roster=RosterType.FREE_AGENCY, # Conceptually joining from outside from_roster=RosterType.FREE_AGENCY, # Conceptually joining from outside
to_roster=to_roster, to_roster=to_roster,
from_team=None, from_team=None,
to_team=to_team to_team=to_team,
) )
# Add moves to respective builders # Add moves to respective builders
# Skip pending transaction check for trades - they have their own validation workflow # Skip pending transaction check for trades - they have their own validation workflow
from_success, from_error = await from_builder.add_move(from_move, check_pending_transactions=False) from_success, from_error = await from_builder.add_move(
from_move, check_pending_transactions=False
)
if not from_success: if not from_success:
# Remove from trade if builder failed # Remove from trade if builder failed
from_participant.moves_giving.remove(trade_move) from_participant.moves_giving.remove(trade_move)
to_participant.moves_receiving.remove(trade_move) to_participant.moves_receiving.remove(trade_move)
return False, f"Error adding move to {from_team.abbrev}: {from_error}" return False, f"Error adding move to {from_team.abbrev}: {from_error}"
to_success, to_error = await to_builder.add_move(to_move, check_pending_transactions=False) to_success, to_error = await to_builder.add_move(
to_move, check_pending_transactions=False
)
if not to_success: if not to_success:
# Rollback both if second failed # Rollback both if second failed
from_builder.remove_move(player.id) from_builder.remove_move(player.id)
@ -332,15 +372,13 @@ class TradeBuilder:
to_participant.moves_receiving.remove(trade_move) to_participant.moves_receiving.remove(trade_move)
return False, f"Error adding move to {to_team.abbrev}: {to_error}" return False, f"Error adding move to {to_team.abbrev}: {to_error}"
logger.info(f"Added player move to trade {self.trade_id}: {trade_move.description}") logger.info(
f"Added player move to trade {self.trade_id}: {trade_move.description}"
)
return True, "" return True, ""
async def add_supplementary_move( async def add_supplementary_move(
self, self, team: Team, player: Player, from_roster: RosterType, to_roster: RosterType
team: Team,
player: Player,
from_roster: RosterType,
to_roster: RosterType
) -> tuple[bool, str]: ) -> tuple[bool, str]:
""" """
Add a supplementary move (internal organizational move) for roster legality. Add a supplementary move (internal organizational move) for roster legality.
@ -366,7 +404,7 @@ class TradeBuilder:
from_team=team, from_team=team,
to_team=team, to_team=team,
source_team=team, source_team=team,
destination_team=team destination_team=team,
) )
# Add to participant's supplementary moves # Add to participant's supplementary moves
@ -379,16 +417,20 @@ class TradeBuilder:
from_roster=from_roster, from_roster=from_roster,
to_roster=to_roster, to_roster=to_roster,
from_team=team, from_team=team,
to_team=team to_team=team,
) )
# Skip pending transaction check for trade supplementary moves # Skip pending transaction check for trade supplementary moves
success, error = await builder.add_move(trans_move, check_pending_transactions=False) success, error = await builder.add_move(
trans_move, check_pending_transactions=False
)
if not success: if not success:
participant.supplementary_moves.remove(supp_move) participant.supplementary_moves.remove(supp_move)
return False, error return False, error
logger.info(f"Added supplementary move for {team.abbrev}: {supp_move.description}") logger.info(
f"Added supplementary move for {team.abbrev}: {supp_move.description}"
)
return True, "" return True, ""
async def remove_move(self, player_id: int) -> tuple[bool, str]: async def remove_move(self, player_id: int) -> tuple[bool, str]:
@ -432,21 +474,41 @@ class TradeBuilder:
for builder in self._team_builders.values(): for builder in self._team_builders.values():
builder.remove_move(player_id) builder.remove_move(player_id)
logger.info(f"Removed move from trade {self.trade_id}: {removed_move.description}") logger.info(
f"Removed move from trade {self.trade_id}: {removed_move.description}"
)
return True, "" return True, ""
async def validate_trade(self, next_week: Optional[int] = None) -> TradeValidationResult: async def validate_trade(
self, next_week: Optional[int] = None
) -> TradeValidationResult:
""" """
Validate the entire trade including all teams' roster legality. Validate the entire trade including all teams' roster legality.
Validates against next week's projected roster (current roster + pending
transactions), matching the behavior of /dropadd validation.
Args: Args:
next_week: Week to validate for (optional) next_week: Week to validate for (auto-fetched if not provided)
Returns: Returns:
TradeValidationResult with comprehensive validation TradeValidationResult with comprehensive validation
""" """
result = TradeValidationResult() result = TradeValidationResult()
# Auto-fetch next week so validation includes pending transactions
if next_week is None:
try:
from services.league_service import league_service
current_state = await league_service.get_current_state()
next_week = (current_state.week + 1) if current_state else 1
except Exception as e:
logger.warning(
f"Could not determine next week for trade validation: {e}"
)
next_week = None
# Validate trade structure # Validate trade structure
is_balanced, balance_errors = self.trade.validate_trade_balance() is_balanced, balance_errors = self.trade.validate_trade_balance()
if not is_balanced: if not is_balanced:
@ -472,13 +534,17 @@ class TradeBuilder:
if self.team_count < 2: if self.team_count < 2:
result.trade_suggestions.append("Add another team to create a trade") result.trade_suggestions.append("Add another team to create a trade")
logger.debug(f"Trade validation for {self.trade_id}: Legal={result.is_legal}, Errors={len(result.all_errors)}") logger.debug(
f"Trade validation for {self.trade_id}: Legal={result.is_legal}, Errors={len(result.all_errors)}"
)
return result return result
def _get_or_create_builder(self, team: Team) -> TransactionBuilder: def _get_or_create_builder(self, team: Team) -> TransactionBuilder:
"""Get or create a transaction builder for a team.""" """Get or create a transaction builder for a team."""
if team.id not in self._team_builders: if team.id not in self._team_builders:
self._team_builders[team.id] = TransactionBuilder(team, self.trade.initiated_by, self.trade.season) self._team_builders[team.id] = TransactionBuilder(
team, self.trade.initiated_by, self.trade.season
)
return self._team_builders[team.id] return self._team_builders[team.id]
def clear_trade(self) -> None: def clear_trade(self) -> None:
@ -592,4 +658,4 @@ def clear_trade_builder_by_team(team_id: int) -> bool:
def get_active_trades() -> Dict[str, TradeBuilder]: def get_active_trades() -> Dict[str, TradeBuilder]:
"""Get all active trade builders (for debugging/admin purposes).""" """Get all active trade builders (for debugging/admin purposes)."""
return _active_trade_builders.copy() return _active_trade_builders.copy()

View File

@ -3,6 +3,7 @@ Interactive Trade Embed Views
Handles the Discord embed and button interfaces for the multi-team trade builder. Handles the Discord embed and button interfaces for the multi-team trade builder.
""" """
import discord import discord
from typing import Optional, List from typing import Optional, List
from datetime import datetime, timezone from datetime import datetime, timezone
@ -31,60 +32,56 @@ class TradeEmbedView(discord.ui.View):
"""Check if user has permission to interact with this view.""" """Check if user has permission to interact with this 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 trade builder.", "You don't have permission to use this trade builder.",
ephemeral=True ephemeral=True,
) )
return False return False
return True return True
async def on_timeout(self) -> None: async def on_timeout(self) -> None:
"""Handle view timeout.""" """Handle view timeout."""
# Disable all buttons when timeout occurs
for item in self.children: for item in self.children:
if isinstance(item, discord.ui.Button): if isinstance(item, discord.ui.Button):
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)
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
# Create select menu for move removal
select_view = RemoveTradeMovesView(self.builder, self.user_id) select_view = RemoveTradeMovesView(self.builder, self.user_id)
embed = await create_trade_embed(self.builder) embed = await create_trade_embed(self.builder)
await interaction.response.edit_message(embed=embed, view=select_view) await interaction.response.edit_message(embed=embed, view=select_view)
@discord.ui.button(label="Validate Trade", style=discord.ButtonStyle.secondary, emoji="🔍") @discord.ui.button(label="Validate Trade", style=discord.ButtonStyle.secondary)
async def validate_button(self, interaction: discord.Interaction, button: discord.ui.Button): async def validate_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle validate trade button click.""" """Handle validate trade button click."""
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
# Perform detailed validation
validation = await self.builder.validate_trade() validation = await self.builder.validate_trade()
# Create validation report
if validation.is_legal: if validation.is_legal:
status_emoji = ""
status_text = "**Trade is LEGAL**" status_text = "**Trade is LEGAL**"
color = EmbedColors.SUCCESS color = EmbedColors.SUCCESS
else: else:
status_emoji = ""
status_text = "**Trade has ERRORS**" status_text = "**Trade has ERRORS**"
color = EmbedColors.ERROR color = EmbedColors.ERROR
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"{status_emoji} Trade Validation Report", title="Trade Validation Report",
description=status_text, description=status_text,
color=color color=color,
) )
# Add team-by-team validation
for participant in self.builder.trade.participants: for participant in self.builder.trade.participants:
team_validation = validation.get_participant_validation(participant.team.id) team_validation = validation.get_participant_validation(participant.team.id)
if team_validation: if team_validation:
@ -98,72 +95,65 @@ class TradeEmbedView(discord.ui.View):
team_status.append(team_validation.pre_existing_transactions_note) team_status.append(team_validation.pre_existing_transactions_note)
embed.add_field( embed.add_field(
name=f"🏟️ {participant.team.abbrev} - {participant.team.sname}", name=f"{participant.team.abbrev} - {participant.team.sname}",
value="\n".join(team_status), value="\n".join(team_status),
inline=False inline=False,
) )
# Add overall errors and suggestions
if validation.all_errors: if validation.all_errors:
error_text = "\n".join([f"{error}" for error in validation.all_errors]) error_text = "\n".join([f"- {error}" for error in validation.all_errors])
embed.add_field( embed.add_field(name="Errors", value=error_text, inline=False)
name="❌ Errors",
value=error_text,
inline=False
)
if validation.all_suggestions: if validation.all_suggestions:
suggestion_text = "\n".join([f"💡 {suggestion}" for suggestion in validation.all_suggestions]) suggestion_text = "\n".join(
embed.add_field( [f"- {suggestion}" for suggestion in validation.all_suggestions]
name="💡 Suggestions",
value=suggestion_text,
inline=False
) )
embed.add_field(name="Suggestions", value=suggestion_text, inline=False)
await interaction.followup.send(embed=embed, ephemeral=True) await interaction.followup.send(embed=embed, ephemeral=True)
@discord.ui.button(label="Submit Trade", style=discord.ButtonStyle.primary, emoji="📤") @discord.ui.button(label="Submit Trade", style=discord.ButtonStyle.primary)
async def submit_button(self, interaction: discord.Interaction, button: discord.ui.Button): async def submit_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle submit trade button click.""" """Handle submit trade 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 trade. Add some moves first!", "Cannot submit empty trade. Add some moves first!", ephemeral=True
ephemeral=True
) )
return return
# Validate before submission
validation = await self.builder.validate_trade() validation = await self.builder.validate_trade()
if not validation.is_legal: if not validation.is_legal:
error_msg = "**Cannot submit illegal trade:**\n" error_msg = "**Cannot submit illegal trade:**\n"
error_msg += "\n".join([f" {error}" for error in validation.all_errors]) error_msg += "\n".join([f"- {error}" for error in validation.all_errors])
if validation.all_suggestions: if validation.all_suggestions:
error_msg += "\n\n**Suggestions:**\n" error_msg += "\n\n**Suggestions:**\n"
error_msg += "\n".join([f"💡 {suggestion}" for suggestion in validation.all_suggestions]) error_msg += "\n".join(
[f"- {suggestion}" for suggestion in validation.all_suggestions]
)
await interaction.response.send_message(error_msg, ephemeral=True) await interaction.response.send_message(error_msg, ephemeral=True)
return return
# Show confirmation modal
modal = SubmitTradeConfirmationModal(self.builder) modal = SubmitTradeConfirmationModal(self.builder)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@discord.ui.button(label="Cancel Trade", style=discord.ButtonStyle.secondary, emoji="") @discord.ui.button(label="Cancel Trade", style=discord.ButtonStyle.secondary)
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 trade button click.""" """Handle cancel trade button click."""
self.builder.clear_trade() self.builder.clear_trade()
embed = await create_trade_embed(self.builder) embed = await create_trade_embed(self.builder)
# Disable all buttons after cancellation
for item in self.children: for item in self.children:
if isinstance(item, discord.ui.Button): if isinstance(item, discord.ui.Button):
item.disabled = True item.disabled = True
await interaction.response.edit_message( await interaction.response.edit_message(
content="❌ **Trade cancelled and cleared.**", content="**Trade cancelled and cleared.**", embed=embed, view=self
embed=embed,
view=self
) )
self.stop() self.stop()
@ -176,12 +166,12 @@ class RemoveTradeMovesView(discord.ui.View):
self.builder = builder self.builder = builder
self.user_id = user_id self.user_id = user_id
# Create select menu with current moves
if not builder.is_empty: if not builder.is_empty:
self.add_item(RemoveTradeMovesSelect(builder)) self.add_item(RemoveTradeMovesSelect(builder))
# Add back button back_button = discord.ui.Button(
back_button = discord.ui.Button(label="Back", style=discord.ButtonStyle.secondary, emoji="⬅️") label="Back", style=discord.ButtonStyle.secondary
)
back_button.callback = self.back_callback back_button.callback = self.back_callback
self.add_item(back_button) self.add_item(back_button)
@ -202,35 +192,36 @@ class RemoveTradeMovesSelect(discord.ui.Select):
def __init__(self, builder: TradeBuilder): def __init__(self, builder: TradeBuilder):
self.builder = builder self.builder = builder
# Create options from all moves (cross-team and supplementary)
options = [] options = []
move_count = 0 move_count = 0
# Add cross-team moves for move in builder.trade.cross_team_moves[
for move in builder.trade.cross_team_moves[:20]: # Limit to avoid Discord's 25 option limit :20
options.append(discord.SelectOption( ]: # Limit to avoid Discord's 25 option limit
label=f"{move.player.name}", options.append(
description=move.description[:100], # Discord description limit discord.SelectOption(
value=str(move.player.id), label=f"{move.player.name}",
emoji="🔄" description=move.description[:100],
)) value=str(move.player.id),
)
)
move_count += 1 move_count += 1
# Add supplementary moves if there's room
remaining_slots = 25 - move_count remaining_slots = 25 - move_count
for move in builder.trade.supplementary_moves[:remaining_slots]: for move in builder.trade.supplementary_moves[:remaining_slots]:
options.append(discord.SelectOption( options.append(
label=f"{move.player.name}", discord.SelectOption(
description=move.description[:100], label=f"{move.player.name}",
value=str(move.player.id), description=move.description[:100],
emoji="⚙️" 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):
@ -241,27 +232,25 @@ class RemoveTradeMovesSelect(discord.ui.Select):
if success: if success:
await interaction.response.send_message( await interaction.response.send_message(
f"✅ Removed move for player ID {player_id}", f"Removed move for player ID {player_id}", ephemeral=True
ephemeral=True
) )
# Update the embed
main_view = TradeEmbedView(self.builder, interaction.user.id) main_view = TradeEmbedView(self.builder, interaction.user.id)
embed = await create_trade_embed(self.builder) embed = await create_trade_embed(self.builder)
# 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(
f"❌ Could not remove move: {error_msg}", f"Could not remove move: {error_msg}", ephemeral=True
ephemeral=True
) )
class SubmitTradeConfirmationModal(discord.ui.Modal): class SubmitTradeConfirmationModal(discord.ui.Modal):
"""Modal for confirming trade submission - posts acceptance request to trade channel.""" """Modal for confirming trade submission - posts acceptance request to trade channel."""
def __init__(self, builder: TradeBuilder, trade_channel: Optional[discord.TextChannel] = None): def __init__(
self, builder: TradeBuilder, trade_channel: Optional[discord.TextChannel] = None
):
super().__init__(title="Confirm Trade Submission") super().__init__(title="Confirm Trade Submission")
self.builder = builder self.builder = builder
self.trade_channel = trade_channel self.trade_channel = trade_channel
@ -270,7 +259,7 @@ class SubmitTradeConfirmationModal(discord.ui.Modal):
label="Type 'CONFIRM' to submit for approval", label="Type 'CONFIRM' to submit for approval",
placeholder="CONFIRM", placeholder="CONFIRM",
required=True, required=True,
max_length=7 max_length=7,
) )
self.add_item(self.confirmation) self.add_item(self.confirmation)
@ -279,56 +268,52 @@ class SubmitTradeConfirmationModal(discord.ui.Modal):
"""Handle confirmation submission - posts acceptance view to trade channel.""" """Handle confirmation submission - posts acceptance view to trade channel."""
if self.confirmation.value.upper() != "CONFIRM": if self.confirmation.value.upper() != "CONFIRM":
await interaction.response.send_message( await interaction.response.send_message(
"Trade not submitted. You must type 'CONFIRM' exactly.", "Trade 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:
# Update trade status to PROPOSED
from models.trade import TradeStatus from models.trade import TradeStatus
self.builder.trade.status = TradeStatus.PROPOSED self.builder.trade.status = TradeStatus.PROPOSED
# Create acceptance embed and view
acceptance_embed = await create_trade_acceptance_embed(self.builder) acceptance_embed = await create_trade_acceptance_embed(self.builder)
acceptance_view = TradeAcceptanceView(self.builder) acceptance_view = TradeAcceptanceView(self.builder)
# Find the trade channel to post to
channel = self.trade_channel channel = self.trade_channel
if not channel: if not channel:
# Try to find trade channel by name pattern
trade_channel_name = f"trade-{'-'.join(t.abbrev.lower() for t in self.builder.participating_teams)}"
for ch in interaction.guild.text_channels: # type: ignore for ch in interaction.guild.text_channels: # type: ignore
if ch.name.startswith("trade-") and self.builder.trade_id[:4] in ch.name: if (
ch.name.startswith("trade-")
and self.builder.trade_id[:4] in ch.name
):
channel = ch channel = ch
break break
if channel: if channel:
# Post acceptance request to trade channel
await channel.send( await channel.send(
content="📋 **Trade submitted for approval!** All teams must accept to complete the trade.", content="**Trade submitted for approval.** All teams must accept to complete the trade.",
embed=acceptance_embed, embed=acceptance_embed,
view=acceptance_view view=acceptance_view,
) )
await interaction.followup.send( await interaction.followup.send(
f"✅ Trade submitted for approval!\n\nThe acceptance request has been posted to {channel.mention}.\n" f"Trade submitted for approval.\n\nThe acceptance request has been posted to {channel.mention}.\n"
f"All participating teams must click **Accept Trade** to finalize.", f"All participating teams must click **Accept Trade** to finalize.",
ephemeral=True ephemeral=True,
) )
else: else:
# No trade channel found, post in current channel
await interaction.followup.send( await interaction.followup.send(
content="📋 **Trade submitted for approval!** All teams must accept to complete the trade.", content="**Trade submitted for approval.** All teams must accept to complete the trade.",
embed=acceptance_embed, embed=acceptance_embed,
view=acceptance_view view=acceptance_view,
) )
except Exception as e: except Exception as e:
await interaction.followup.send( await interaction.followup.send(
f"❌ Error submitting trade: {str(e)}", f"Error submitting trade: {str(e)}", ephemeral=True
ephemeral=True
) )
@ -343,8 +328,11 @@ class TradeAcceptanceView(discord.ui.View):
"""Get the team owned by the interacting user.""" """Get the team owned by the interacting user."""
from services.team_service import team_service from services.team_service import team_service
from config import get_config from config import get_config
config = get_config() config = get_config()
return await team_service.get_team_by_owner(interaction.user.id, config.sba_season) return await team_service.get_team_by_owner(
interaction.user.id, config.sba_season
)
async def interaction_check(self, interaction: discord.Interaction) -> bool: async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Check if user is a GM of a participating team.""" """Check if user is a GM of a participating team."""
@ -352,17 +340,14 @@ class TradeAcceptanceView(discord.ui.View):
if not user_team: if not user_team:
await interaction.response.send_message( await interaction.response.send_message(
"❌ You don't own a team in the league.", "You don't own a team in the league.", ephemeral=True
ephemeral=True
) )
return False return False
# Check if their team (or organization) is participating
participant = self.builder.trade.get_participant_by_organization(user_team) participant = self.builder.trade.get_participant_by_organization(user_team)
if not participant: if not participant:
await interaction.response.send_message( await interaction.response.send_message(
"❌ Your team is not part of this trade.", "Your team is not part of this trade.", ephemeral=True
ephemeral=True
) )
return False return False
@ -374,47 +359,45 @@ class TradeAcceptanceView(discord.ui.View):
if isinstance(item, discord.ui.Button): if isinstance(item, discord.ui.Button):
item.disabled = True item.disabled = True
@discord.ui.button(label="Accept Trade", style=discord.ButtonStyle.success, emoji="") @discord.ui.button(label="Accept Trade", style=discord.ButtonStyle.success)
async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button): async def accept_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle accept button click.""" """Handle accept button click."""
user_team = await self._get_user_team(interaction) user_team = await self._get_user_team(interaction)
if not user_team: if not user_team:
return return
# Find the participating team (could be org affiliate)
participant = self.builder.trade.get_participant_by_organization(user_team) participant = self.builder.trade.get_participant_by_organization(user_team)
if not participant: if not participant:
return return
team_id = participant.team.id team_id = participant.team.id
# Check if already accepted
if self.builder.has_team_accepted(team_id): if self.builder.has_team_accepted(team_id):
await interaction.response.send_message( await interaction.response.send_message(
f"{participant.team.abbrev} has already accepted this trade.", f"{participant.team.abbrev} has already accepted this trade.",
ephemeral=True ephemeral=True,
) )
return return
# Record acceptance
all_accepted = self.builder.accept_trade(team_id) all_accepted = self.builder.accept_trade(team_id)
if all_accepted: if all_accepted:
# All teams accepted - finalize the trade
await self._finalize_trade(interaction) await self._finalize_trade(interaction)
else: else:
# Update embed to show new acceptance status
embed = await create_trade_acceptance_embed(self.builder) embed = await create_trade_acceptance_embed(self.builder)
await interaction.response.edit_message(embed=embed, view=self) await interaction.response.edit_message(embed=embed, view=self)
# Send confirmation to channel
await interaction.followup.send( await interaction.followup.send(
f"**{participant.team.abbrev}** has accepted the trade! " f"**{participant.team.abbrev}** has accepted the trade. "
f"({len(self.builder.accepted_teams)}/{self.builder.team_count} teams)" f"({len(self.builder.accepted_teams)}/{self.builder.team_count} teams)"
) )
@discord.ui.button(label="Reject Trade", style=discord.ButtonStyle.danger, emoji="") @discord.ui.button(label="Reject Trade", style=discord.ButtonStyle.danger)
async def reject_button(self, interaction: discord.Interaction, button: discord.ui.Button): async def reject_button(
self, interaction: discord.Interaction, button: discord.ui.Button
):
"""Handle reject button click - moves trade back to DRAFT.""" """Handle reject button click - moves trade back to DRAFT."""
user_team = await self._get_user_team(interaction) user_team = await self._get_user_team(interaction)
if not user_team: if not user_team:
@ -424,20 +407,16 @@ class TradeAcceptanceView(discord.ui.View):
if not participant: if not participant:
return return
# Reject the trade
self.builder.reject_trade() self.builder.reject_trade()
# Disable buttons
self.accept_button.disabled = True self.accept_button.disabled = True
self.reject_button.disabled = True self.reject_button.disabled = True
# Update embed to show rejection
embed = await create_trade_rejection_embed(self.builder, participant.team) embed = await create_trade_rejection_embed(self.builder, participant.team)
await interaction.response.edit_message(embed=embed, view=self) await interaction.response.edit_message(embed=embed, view=self)
# Notify the channel
await interaction.followup.send( await interaction.followup.send(
f"**{participant.team.abbrev}** has rejected the trade.\n\n" f"**{participant.team.abbrev}** has rejected the trade.\n\n"
f"The trade has been moved back to **DRAFT** status. " f"The trade has been moved back to **DRAFT** status. "
f"Teams can continue negotiating using `/trade` commands." f"Teams can continue negotiating using `/trade` commands."
) )
@ -459,41 +438,52 @@ class TradeAcceptanceView(discord.ui.View):
config = get_config() config = get_config()
# Get next week for transactions
current = await league_service.get_current_state() current = await league_service.get_current_state()
next_week = current.week + 1 if current else 1 next_week = current.week + 1 if current else 1
# Create FA team for reference
fa_team = Team( fa_team = Team(
id=config.free_agent_team_id, id=config.free_agent_team_id,
abbrev="FA", abbrev="FA",
sname="Free Agents", sname="Free Agents",
lname="Free Agency", lname="Free Agency",
season=self.builder.trade.season season=self.builder.trade.season,
) # type: ignore ) # type: ignore
# Create transactions from all moves
transactions: List[Transaction] = [] transactions: List[Transaction] = []
move_id = f"Trade-{self.builder.trade_id}-{int(datetime.now(timezone.utc).timestamp())}" move_id = f"Trade-{self.builder.trade_id}-{int(datetime.now(timezone.utc).timestamp())}"
# Process cross-team moves
for move in self.builder.trade.cross_team_moves: for move in self.builder.trade.cross_team_moves:
# Get actual team affiliates for from/to based on roster type
if move.from_roster == RosterType.MAJOR_LEAGUE: if move.from_roster == RosterType.MAJOR_LEAGUE:
old_team = move.source_team old_team = move.source_team
elif move.from_roster == RosterType.MINOR_LEAGUE: elif move.from_roster == RosterType.MINOR_LEAGUE:
old_team = await move.source_team.minor_league_affiliate() if move.source_team else None old_team = (
await move.source_team.minor_league_affiliate()
if move.source_team
else None
)
elif move.from_roster == RosterType.INJURED_LIST: elif move.from_roster == RosterType.INJURED_LIST:
old_team = await move.source_team.injured_list_affiliate() if move.source_team else None old_team = (
await move.source_team.injured_list_affiliate()
if move.source_team
else None
)
else: else:
old_team = move.source_team old_team = move.source_team
if move.to_roster == RosterType.MAJOR_LEAGUE: if move.to_roster == RosterType.MAJOR_LEAGUE:
new_team = move.destination_team new_team = move.destination_team
elif move.to_roster == RosterType.MINOR_LEAGUE: elif move.to_roster == RosterType.MINOR_LEAGUE:
new_team = await move.destination_team.minor_league_affiliate() if move.destination_team else None new_team = (
await move.destination_team.minor_league_affiliate()
if move.destination_team
else None
)
elif move.to_roster == RosterType.INJURED_LIST: elif move.to_roster == RosterType.INJURED_LIST:
new_team = await move.destination_team.injured_list_affiliate() if move.destination_team else None new_team = (
await move.destination_team.injured_list_affiliate()
if move.destination_team
else None
)
else: else:
new_team = move.destination_team new_team = move.destination_team
@ -507,18 +497,25 @@ class TradeAcceptanceView(discord.ui.View):
oldteam=old_team, oldteam=old_team,
newteam=new_team, newteam=new_team,
cancelled=False, cancelled=False,
frozen=False # Trades are NOT frozen - immediately effective frozen=False,
) )
transactions.append(transaction) transactions.append(transaction)
# Process supplementary moves
for move in self.builder.trade.supplementary_moves: for move in self.builder.trade.supplementary_moves:
if move.from_roster == RosterType.MAJOR_LEAGUE: if move.from_roster == RosterType.MAJOR_LEAGUE:
old_team = move.source_team old_team = move.source_team
elif move.from_roster == RosterType.MINOR_LEAGUE: elif move.from_roster == RosterType.MINOR_LEAGUE:
old_team = await move.source_team.minor_league_affiliate() if move.source_team else None old_team = (
await move.source_team.minor_league_affiliate()
if move.source_team
else None
)
elif move.from_roster == RosterType.INJURED_LIST: elif move.from_roster == RosterType.INJURED_LIST:
old_team = await move.source_team.injured_list_affiliate() if move.source_team else None old_team = (
await move.source_team.injured_list_affiliate()
if move.source_team
else None
)
elif move.from_roster == RosterType.FREE_AGENCY: elif move.from_roster == RosterType.FREE_AGENCY:
old_team = fa_team old_team = fa_team
else: else:
@ -527,9 +524,17 @@ class TradeAcceptanceView(discord.ui.View):
if move.to_roster == RosterType.MAJOR_LEAGUE: if move.to_roster == RosterType.MAJOR_LEAGUE:
new_team = move.destination_team new_team = move.destination_team
elif move.to_roster == RosterType.MINOR_LEAGUE: elif move.to_roster == RosterType.MINOR_LEAGUE:
new_team = await move.destination_team.minor_league_affiliate() if move.destination_team else None new_team = (
await move.destination_team.minor_league_affiliate()
if move.destination_team
else None
)
elif move.to_roster == RosterType.INJURED_LIST: elif move.to_roster == RosterType.INJURED_LIST:
new_team = await move.destination_team.injured_list_affiliate() if move.destination_team else None new_team = (
await move.destination_team.injured_list_affiliate()
if move.destination_team
else None
)
elif move.to_roster == RosterType.FREE_AGENCY: elif move.to_roster == RosterType.FREE_AGENCY:
new_team = fa_team new_team = fa_team
else: else:
@ -545,45 +550,42 @@ class TradeAcceptanceView(discord.ui.View):
oldteam=old_team, oldteam=old_team,
newteam=new_team, newteam=new_team,
cancelled=False, cancelled=False,
frozen=False # Trades are NOT frozen - immediately effective frozen=False,
) )
transactions.append(transaction) transactions.append(transaction)
# POST transactions to database
if transactions: if transactions:
created_transactions = await transaction_service.create_transaction_batch(transactions) created_transactions = (
await transaction_service.create_transaction_batch(transactions)
)
else: else:
created_transactions = [] created_transactions = []
# Post to #transaction-log channel
if created_transactions and interaction.client: if created_transactions and interaction.client:
await post_trade_to_log( await post_trade_to_log(
bot=interaction.client, bot=interaction.client,
builder=self.builder, builder=self.builder,
transactions=created_transactions, transactions=created_transactions,
effective_week=next_week effective_week=next_week,
) )
# Update trade status
self.builder.trade.status = TradeStatus.ACCEPTED self.builder.trade.status = TradeStatus.ACCEPTED
# Disable buttons
self.accept_button.disabled = True self.accept_button.disabled = True
self.reject_button.disabled = True self.reject_button.disabled = True
# Update embed to show completion embed = await create_trade_complete_embed(
embed = await create_trade_complete_embed(self.builder, len(created_transactions), next_week) self.builder, len(created_transactions), next_week
)
await interaction.edit_original_response(embed=embed, view=self) await interaction.edit_original_response(embed=embed, view=self)
# Send completion message
await interaction.followup.send( await interaction.followup.send(
f"🎉 **Trade Complete!**\n\n" f"**Trade Complete!**\n\n"
f"All {self.builder.team_count} teams have accepted the trade.\n" f"All {self.builder.team_count} teams have accepted the trade.\n"
f"**{len(created_transactions)} transactions** have been created for **Week {next_week}**.\n\n" f"**{len(created_transactions)} transactions** have been created for **Week {next_week}**.\n\n"
f"Trade ID: `{self.builder.trade_id}`" f"Trade ID: `{self.builder.trade_id}`"
) )
# Clear the trade builder
for team in self.builder.participating_teams: for team in self.builder.participating_teams:
clear_trade_builder_by_team(team.id) clear_trade_builder_by_team(team.id)
@ -591,81 +593,79 @@ class TradeAcceptanceView(discord.ui.View):
except Exception as e: except Exception as e:
await interaction.followup.send( await interaction.followup.send(
f"❌ Error finalizing trade: {str(e)}", f"Error finalizing trade: {str(e)}", ephemeral=True
ephemeral=True
) )
async def create_trade_acceptance_embed(builder: TradeBuilder) -> discord.Embed: async def create_trade_acceptance_embed(builder: TradeBuilder) -> discord.Embed:
"""Create embed showing trade details and acceptance status.""" """Create embed showing trade details and acceptance status."""
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"📋 Trade Pending Acceptance - {builder.trade.get_trade_summary()}", title=f"Trade Pending Acceptance - {builder.trade.get_trade_summary()}",
description="All participating teams must accept to complete the trade.", description="All participating teams must accept to complete the trade.",
color=EmbedColors.WARNING color=EmbedColors.WARNING,
) )
# Show participating teams team_list = [
team_list = [f"{team.abbrev} - {team.sname}" for team in builder.participating_teams] f"- {team.abbrev} - {team.sname}" for team in builder.participating_teams
]
embed.add_field( embed.add_field(
name=f"🏟️ Participating Teams ({builder.team_count})", name=f"Participating Teams ({builder.team_count})",
value="\n".join(team_list), value="\n".join(team_list),
inline=False inline=False,
) )
# Show cross-team moves
if builder.trade.cross_team_moves: if builder.trade.cross_team_moves:
moves_text = "" moves_text = ""
for move in builder.trade.cross_team_moves[:10]: for move in builder.trade.cross_team_moves[:10]:
moves_text += f" {move.description}\n" moves_text += f"- {move.description}\n"
if len(builder.trade.cross_team_moves) > 10: if len(builder.trade.cross_team_moves) > 10:
moves_text += f"... and {len(builder.trade.cross_team_moves) - 10} more" moves_text += f"... and {len(builder.trade.cross_team_moves) - 10} more"
embed.add_field( embed.add_field(
name=f"🔄 Player Exchanges ({len(builder.trade.cross_team_moves)})", name=f"Player Exchanges ({len(builder.trade.cross_team_moves)})",
value=moves_text, value=moves_text,
inline=False inline=False,
) )
# Show supplementary moves if any
if builder.trade.supplementary_moves: if builder.trade.supplementary_moves:
supp_text = "" supp_text = ""
for move in builder.trade.supplementary_moves[:5]: for move in builder.trade.supplementary_moves[:5]:
supp_text += f" {move.description}\n" supp_text += f"- {move.description}\n"
if len(builder.trade.supplementary_moves) > 5: if len(builder.trade.supplementary_moves) > 5:
supp_text += f"... and {len(builder.trade.supplementary_moves) - 5} more" supp_text += f"... and {len(builder.trade.supplementary_moves) - 5} more"
embed.add_field( embed.add_field(
name=f"⚙️ Supplementary Moves ({len(builder.trade.supplementary_moves)})", name=f"Supplementary Moves ({len(builder.trade.supplementary_moves)})",
value=supp_text, value=supp_text,
inline=False inline=False,
) )
# Show acceptance status
status_lines = [] status_lines = []
for team in builder.participating_teams: for team in builder.participating_teams:
if team.id in builder.accepted_teams: if team.id in builder.accepted_teams:
status_lines.append(f"**{team.abbrev}** - Accepted") status_lines.append(f"**{team.abbrev}** - Accepted")
else: else:
status_lines.append(f"**{team.abbrev}** - Pending") status_lines.append(f"**{team.abbrev}** - Pending")
embed.add_field( embed.add_field(
name="📊 Acceptance Status", name="Acceptance Status", value="\n".join(status_lines), inline=False
value="\n".join(status_lines),
inline=False
) )
# Add footer embed.set_footer(
embed.set_footer(text=f"Trade ID: {builder.trade_id}{len(builder.accepted_teams)}/{builder.team_count} teams accepted") text=f"Trade ID: {builder.trade_id} | {len(builder.accepted_teams)}/{builder.team_count} teams accepted"
)
return embed return embed
async def create_trade_rejection_embed(builder: TradeBuilder, rejecting_team: Team) -> discord.Embed: async def create_trade_rejection_embed(
builder: TradeBuilder, rejecting_team: Team
) -> discord.Embed:
"""Create embed showing trade was rejected.""" """Create embed showing trade was rejected."""
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"Trade Rejected - {builder.trade.get_trade_summary()}", title=f"Trade Rejected - {builder.trade.get_trade_summary()}",
description=f"**{rejecting_team.abbrev}** has rejected the trade.\n\n" description=f"**{rejecting_team.abbrev}** has rejected the trade.\n\n"
f"The trade has been moved back to **DRAFT** status.\n" f"The trade has been moved back to **DRAFT** status.\n"
f"Teams can continue negotiating using `/trade` commands.", f"Teams can continue negotiating using `/trade` commands.",
color=EmbedColors.ERROR color=EmbedColors.ERROR,
) )
embed.set_footer(text=f"Trade ID: {builder.trade_id}") embed.set_footer(text=f"Trade ID: {builder.trade_id}")
@ -673,37 +673,33 @@ async def create_trade_rejection_embed(builder: TradeBuilder, rejecting_team: Te
return embed return embed
async def create_trade_complete_embed(builder: TradeBuilder, transaction_count: int, effective_week: int) -> discord.Embed: async def create_trade_complete_embed(
builder: TradeBuilder, transaction_count: int, effective_week: int
) -> discord.Embed:
"""Create embed showing trade was completed.""" """Create embed showing trade was completed."""
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"🎉 Trade Complete! - {builder.trade.get_trade_summary()}", title=f"Trade Complete - {builder.trade.get_trade_summary()}",
description=f"All {builder.team_count} teams have accepted the trade!\n\n" description=f"All {builder.team_count} teams have accepted the trade.\n\n"
f"**{transaction_count} transactions** created for **Week {effective_week}**.", f"**{transaction_count} transactions** created for **Week {effective_week}**.",
color=EmbedColors.SUCCESS color=EmbedColors.SUCCESS,
) )
# Show final acceptance status (all green) status_lines = [
status_lines = [f"✅ **{team.abbrev}** - Accepted" for team in builder.participating_teams] f"**{team.abbrev}** - Accepted" for team in builder.participating_teams
embed.add_field( ]
name="📊 Final Status", embed.add_field(name="Final Status", value="\n".join(status_lines), inline=False)
value="\n".join(status_lines),
inline=False
)
# Show cross-team moves
if builder.trade.cross_team_moves: if builder.trade.cross_team_moves:
moves_text = "" moves_text = ""
for move in builder.trade.cross_team_moves[:8]: for move in builder.trade.cross_team_moves[:8]:
moves_text += f" {move.description}\n" moves_text += f"- {move.description}\n"
if len(builder.trade.cross_team_moves) > 8: if len(builder.trade.cross_team_moves) > 8:
moves_text += f"... and {len(builder.trade.cross_team_moves) - 8} more" moves_text += f"... and {len(builder.trade.cross_team_moves) - 8} more"
embed.add_field( embed.add_field(name="Player Exchanges", value=moves_text, inline=False)
name=f"🔄 Player Exchanges",
value=moves_text,
inline=False
)
embed.set_footer(text=f"Trade ID: {builder.trade_id} • Effective: Week {effective_week}") embed.set_footer(
text=f"Trade ID: {builder.trade_id} | Effective: Week {effective_week}"
)
return embed return embed
@ -718,7 +714,6 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed:
Returns: Returns:
Discord embed with current trade state Discord embed with current trade state
""" """
# Determine embed color based on trade status
if builder.is_empty: if builder.is_empty:
color = EmbedColors.SECONDARY color = EmbedColors.SECONDARY
else: else:
@ -726,79 +721,79 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed:
color = EmbedColors.SUCCESS if validation.is_legal else EmbedColors.WARNING color = EmbedColors.SUCCESS if validation.is_legal else EmbedColors.WARNING
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"📋 Trade Builder - {builder.trade.get_trade_summary()}", title=f"Trade Builder - {builder.trade.get_trade_summary()}",
description=f"Build your multi-team trade", description="Build your multi-team trade",
color=color color=color,
) )
# Add participating teams section team_list = [
team_list = [f"{team.abbrev} - {team.sname}" for team in builder.participating_teams] f"- {team.abbrev} - {team.sname}" for team in builder.participating_teams
]
embed.add_field( embed.add_field(
name=f"🏟️ Participating Teams ({builder.team_count})", name=f"Participating Teams ({builder.team_count})",
value="\n".join(team_list) if team_list else "*No teams yet*", value="\n".join(team_list) if team_list else "*No teams yet*",
inline=False inline=False,
) )
# Add current moves section
if builder.is_empty: if builder.is_empty:
embed.add_field( embed.add_field(
name="Current Moves", name="Current Moves",
value="*No moves yet. Use the `/trade` commands to build your trade.*", value="*No moves yet. Use the `/trade` commands to build your trade.*",
inline=False inline=False,
) )
else: else:
# Show cross-team moves
if builder.trade.cross_team_moves: if builder.trade.cross_team_moves:
moves_text = "" moves_text = ""
for i, move in enumerate(builder.trade.cross_team_moves[:8], 1): # Limit display for i, move in enumerate(builder.trade.cross_team_moves[:8], 1):
moves_text += f"{i}. {move.description}\n" moves_text += f"{i}. {move.description}\n"
if len(builder.trade.cross_team_moves) > 8: if len(builder.trade.cross_team_moves) > 8:
moves_text += f"... and {len(builder.trade.cross_team_moves) - 8} more" moves_text += f"... and {len(builder.trade.cross_team_moves) - 8} more"
embed.add_field( embed.add_field(
name=f"🔄 Player Exchanges ({len(builder.trade.cross_team_moves)})", name=f"Player Exchanges ({len(builder.trade.cross_team_moves)})",
value=moves_text, value=moves_text,
inline=False inline=False,
) )
# Show supplementary moves
if builder.trade.supplementary_moves: if builder.trade.supplementary_moves:
supp_text = "" supp_text = ""
for i, move in enumerate(builder.trade.supplementary_moves[:5], 1): # Limit display for i, move in enumerate(builder.trade.supplementary_moves[:5], 1):
supp_text += f"{i}. {move.description}\n" supp_text += f"{i}. {move.description}\n"
if len(builder.trade.supplementary_moves) > 5: if len(builder.trade.supplementary_moves) > 5:
supp_text += f"... and {len(builder.trade.supplementary_moves) - 5} more" supp_text += (
f"... and {len(builder.trade.supplementary_moves) - 5} more"
)
embed.add_field( embed.add_field(
name=f"⚙️ Supplementary Moves ({len(builder.trade.supplementary_moves)})", name=f"Supplementary Moves ({len(builder.trade.supplementary_moves)})",
value=supp_text, value=supp_text,
inline=False inline=False,
) )
# Add quick validation summary
validation = await builder.validate_trade() validation = await builder.validate_trade()
if validation.is_legal: if validation.is_legal:
status_text = "Trade appears legal" status_text = "Trade appears legal"
else: else:
error_count = len(validation.all_errors) error_count = len(validation.all_errors)
status_text = f"{error_count} error{'s' if error_count != 1 else ''} found" status_text = f"{error_count} error{'s' if error_count != 1 else ''} found\n"
status_text += "\n".join(f"- {error}" for error in validation.all_errors)
if validation.all_suggestions:
status_text += "\n" + "\n".join(
f"- {s}" for s in validation.all_suggestions
)
embed.add_field(name="Quick Status", value=status_text, inline=False)
embed.add_field( embed.add_field(
name="🔍 Quick Status", name="Build Your Trade",
value=status_text, value="- `/trade add-player` - Add player exchanges\n- `/trade supplementary` - Add internal moves\n- `/trade add-team` - Add more teams",
inline=False inline=False,
) )
# Add instructions for adding more moves embed.set_footer(
embed.add_field( text=f"Trade ID: {builder.trade_id} | Created: {datetime.fromisoformat(builder.trade.created_at).strftime('%H:%M:%S')}"
name=" Build Your Trade",
value="• `/trade add-player` - Add player exchanges\n• `/trade supplementary` - Add internal moves\n• `/trade add-team` - Add more teams",
inline=False
) )
# Add footer with trade ID and timestamp return embed
embed.set_footer(text=f"Trade ID: {builder.trade_id} • Created: {datetime.fromisoformat(builder.trade.created_at).strftime('%H:%M:%S')}")
return embed