diff --git a/services/trade_builder.py b/services/trade_builder.py index 879959f..8ab1d5d 100644 --- a/services/trade_builder.py +++ b/services/trade_builder.py @@ -3,6 +3,7 @@ Trade Builder Service Extends the TransactionBuilder to support multi-team trades and player exchanges. """ + import logging from typing import Dict, List, Optional, Set from datetime import datetime, timezone @@ -12,10 +13,14 @@ from config import get_config from models.trade import Trade, TradeMove, TradeStatus from models.team import Team, RosterType 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 -logger = logging.getLogger(f'{__name__}.TradeBuilder') +logger = logging.getLogger(f"{__name__}.TradeBuilder") class TradeValidationResult: @@ -52,7 +57,9 @@ class TradeValidationResult: suggestions.extend(validation.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.""" return self.participant_validations.get(team_id) @@ -64,7 +71,12 @@ class TradeBuilder: 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. @@ -79,7 +91,7 @@ class TradeBuilder: status=TradeStatus.DRAFT, initiated_by=initiated_by, created_at=datetime.now(timezone.utc).isoformat(), - season=season + season=season, ) # Add the initiating team as first participant @@ -91,7 +103,9 @@ class TradeBuilder: # Track which teams have accepted the trade (team_id -> True) 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 def trade_id(self) -> str: @@ -127,7 +141,11 @@ class TradeBuilder: @property def pending_teams(self) -> List[Team]: """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: """ @@ -140,7 +158,9 @@ class TradeBuilder: True if all teams have now accepted, False otherwise """ 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 def reject_trade(self) -> None: @@ -160,7 +180,9 @@ class TradeBuilder: Returns: 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: """Check if a specific team has accepted.""" @@ -184,7 +206,9 @@ class TradeBuilder: participant = self.trade.add_participant(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 trade_key = f"{self.trade.initiated_by}:trade" @@ -209,7 +233,10 @@ class TradeBuilder: # Check if team has moves - prevent removal if they do 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 removed = self.trade.remove_participant(team_id) @@ -229,7 +256,7 @@ class TradeBuilder: from_team: Team, to_team: Team, from_roster: RosterType, - to_roster: RosterType + to_roster: RosterType, ) -> tuple[bool, str]: """ Add a player move to the trade. @@ -246,7 +273,10 @@ class TradeBuilder: """ # Validate player is not from Free Agency 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 if not player.team_id: @@ -259,7 +289,10 @@ class TradeBuilder: # Check if player's team is in the same organization as 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) from_participant = self.trade.get_participant_by_organization(from_team) @@ -274,7 +307,10 @@ class TradeBuilder: for participant in self.trade.participants: for existing_move in participant.all_moves: 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 trade_move = TradeMove( @@ -284,7 +320,7 @@ class TradeBuilder: from_team=from_team, to_team=to_team, source_team=from_team, - destination_team=to_team + destination_team=to_team, ) # Add to giving team's moves @@ -303,7 +339,7 @@ class TradeBuilder: from_roster=from_roster, to_roster=RosterType.FREE_AGENCY, # Conceptually leaving the org from_team=from_team, - to_team=None + to_team=None, ) # Move for receiving team (player joining) @@ -312,19 +348,23 @@ class TradeBuilder: from_roster=RosterType.FREE_AGENCY, # Conceptually joining from outside to_roster=to_roster, from_team=None, - to_team=to_team + to_team=to_team, ) # Add moves to respective builders # 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: # Remove from trade if builder failed from_participant.moves_giving.remove(trade_move) to_participant.moves_receiving.remove(trade_move) 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: # Rollback both if second failed from_builder.remove_move(player.id) @@ -332,15 +372,13 @@ class TradeBuilder: to_participant.moves_receiving.remove(trade_move) 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, "" async def add_supplementary_move( - self, - team: Team, - player: Player, - from_roster: RosterType, - to_roster: RosterType + self, team: Team, player: Player, from_roster: RosterType, to_roster: RosterType ) -> tuple[bool, str]: """ Add a supplementary move (internal organizational move) for roster legality. @@ -366,7 +404,7 @@ class TradeBuilder: from_team=team, to_team=team, source_team=team, - destination_team=team + destination_team=team, ) # Add to participant's supplementary moves @@ -379,16 +417,20 @@ class TradeBuilder: from_roster=from_roster, to_roster=to_roster, from_team=team, - to_team=team + to_team=team, ) # 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: participant.supplementary_moves.remove(supp_move) 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, "" async def remove_move(self, player_id: int) -> tuple[bool, str]: @@ -432,21 +474,41 @@ class TradeBuilder: for builder in self._team_builders.values(): 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, "" - 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. + Validates against next week's projected roster (current roster + pending + transactions), matching the behavior of /dropadd validation. + Args: - next_week: Week to validate for (optional) + next_week: Week to validate for (auto-fetched if not provided) Returns: TradeValidationResult with comprehensive validation """ 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 is_balanced, balance_errors = self.trade.validate_trade_balance() if not is_balanced: @@ -472,13 +534,17 @@ class TradeBuilder: if self.team_count < 2: 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 def _get_or_create_builder(self, team: Team) -> TransactionBuilder: """Get or create a transaction builder for a team.""" 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] 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]: """Get all active trade builders (for debugging/admin purposes).""" - return _active_trade_builders.copy() \ No newline at end of file + return _active_trade_builders.copy() diff --git a/views/trade_embed.py b/views/trade_embed.py index 507299f..acccfa7 100644 --- a/views/trade_embed.py +++ b/views/trade_embed.py @@ -3,6 +3,7 @@ Interactive Trade Embed Views Handles the Discord embed and button interfaces for the multi-team trade builder. """ + import discord from typing import Optional, List from datetime import datetime, timezone @@ -31,60 +32,56 @@ class TradeEmbedView(discord.ui.View): """Check if user has permission to interact with this view.""" if interaction.user.id != self.user_id: await interaction.response.send_message( - "āŒ You don't have permission to use this trade builder.", - ephemeral=True + "You don't have permission to use this trade builder.", + ephemeral=True, ) return False return True async def on_timeout(self) -> None: """Handle view timeout.""" - # Disable all buttons when timeout occurs for item in self.children: if isinstance(item, discord.ui.Button): item.disabled = True - @discord.ui.button(label="Remove Move", style=discord.ButtonStyle.red, emoji="āž–") - async def remove_move_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Remove Move", style=discord.ButtonStyle.red) + async def remove_move_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): """Handle remove move button click.""" if self.builder.is_empty: await interaction.response.send_message( - "āŒ No moves to remove. Add some moves first!", - ephemeral=True + "No moves to remove. Add some moves first!", ephemeral=True ) return - # Create select menu for move removal select_view = RemoveTradeMovesView(self.builder, self.user_id) embed = await create_trade_embed(self.builder) await interaction.response.edit_message(embed=embed, view=select_view) - @discord.ui.button(label="Validate Trade", style=discord.ButtonStyle.secondary, emoji="šŸ”") - async def validate_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Validate Trade", style=discord.ButtonStyle.secondary) + async def validate_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): """Handle validate trade button click.""" await interaction.response.defer(ephemeral=True) - # Perform detailed validation validation = await self.builder.validate_trade() - # Create validation report if validation.is_legal: - status_emoji = "āœ…" status_text = "**Trade is LEGAL**" color = EmbedColors.SUCCESS else: - status_emoji = "āŒ" status_text = "**Trade has ERRORS**" color = EmbedColors.ERROR embed = EmbedTemplate.create_base_embed( - title=f"{status_emoji} Trade Validation Report", + title="Trade Validation Report", description=status_text, - color=color + color=color, ) - # Add team-by-team validation for participant in self.builder.trade.participants: team_validation = validation.get_participant_validation(participant.team.id) if team_validation: @@ -98,72 +95,65 @@ class TradeEmbedView(discord.ui.View): team_status.append(team_validation.pre_existing_transactions_note) embed.add_field( - name=f"šŸŸļø {participant.team.abbrev} - {participant.team.sname}", + name=f"{participant.team.abbrev} - {participant.team.sname}", value="\n".join(team_status), - inline=False + inline=False, ) - # Add overall errors and suggestions if validation.all_errors: - error_text = "\n".join([f"• {error}" for error in validation.all_errors]) - embed.add_field( - name="āŒ Errors", - value=error_text, - inline=False - ) + error_text = "\n".join([f"- {error}" for error in validation.all_errors]) + embed.add_field(name="Errors", value=error_text, inline=False) if validation.all_suggestions: - suggestion_text = "\n".join([f"šŸ’” {suggestion}" for suggestion in validation.all_suggestions]) - embed.add_field( - name="šŸ’” Suggestions", - value=suggestion_text, - inline=False + suggestion_text = "\n".join( + [f"- {suggestion}" for suggestion in validation.all_suggestions] ) + embed.add_field(name="Suggestions", value=suggestion_text, inline=False) await interaction.followup.send(embed=embed, ephemeral=True) - @discord.ui.button(label="Submit Trade", style=discord.ButtonStyle.primary, emoji="šŸ“¤") - async def submit_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Submit Trade", style=discord.ButtonStyle.primary) + async def submit_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): """Handle submit trade button click.""" if self.builder.is_empty: await interaction.response.send_message( - "āŒ Cannot submit empty trade. Add some moves first!", - ephemeral=True + "Cannot submit empty trade. Add some moves first!", ephemeral=True ) return - # Validate before submission validation = await self.builder.validate_trade() if not validation.is_legal: - error_msg = "āŒ **Cannot submit illegal trade:**\n" - error_msg += "\n".join([f"• {error}" for error in validation.all_errors]) + error_msg = "**Cannot submit illegal trade:**\n" + error_msg += "\n".join([f"- {error}" for error in validation.all_errors]) if validation.all_suggestions: 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) return - # Show confirmation modal modal = SubmitTradeConfirmationModal(self.builder) await interaction.response.send_modal(modal) - @discord.ui.button(label="Cancel Trade", style=discord.ButtonStyle.secondary, emoji="āŒ") - async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Cancel Trade", style=discord.ButtonStyle.secondary) + async def cancel_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): """Handle cancel trade button click.""" self.builder.clear_trade() embed = await create_trade_embed(self.builder) - # Disable all buttons after cancellation for item in self.children: if isinstance(item, discord.ui.Button): item.disabled = True await interaction.response.edit_message( - content="āŒ **Trade cancelled and cleared.**", - embed=embed, - view=self + content="**Trade cancelled and cleared.**", embed=embed, view=self ) self.stop() @@ -176,12 +166,12 @@ class RemoveTradeMovesView(discord.ui.View): self.builder = builder self.user_id = user_id - # Create select menu with current moves if not builder.is_empty: self.add_item(RemoveTradeMovesSelect(builder)) - # 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 + ) back_button.callback = self.back_callback self.add_item(back_button) @@ -202,35 +192,36 @@ class RemoveTradeMovesSelect(discord.ui.Select): def __init__(self, builder: TradeBuilder): self.builder = builder - # Create options from all moves (cross-team and supplementary) options = [] move_count = 0 - # Add cross-team moves - for move in builder.trade.cross_team_moves[:20]: # Limit to avoid Discord's 25 option limit - options.append(discord.SelectOption( - label=f"{move.player.name}", - description=move.description[:100], # Discord description limit - value=str(move.player.id), - emoji="šŸ”„" - )) + for move in builder.trade.cross_team_moves[ + :20 + ]: # Limit to avoid Discord's 25 option limit + options.append( + discord.SelectOption( + label=f"{move.player.name}", + description=move.description[:100], + value=str(move.player.id), + ) + ) move_count += 1 - # Add supplementary moves if there's room remaining_slots = 25 - move_count for move in builder.trade.supplementary_moves[:remaining_slots]: - options.append(discord.SelectOption( - label=f"{move.player.name}", - description=move.description[:100], - value=str(move.player.id), - emoji="āš™ļø" - )) + options.append( + discord.SelectOption( + label=f"{move.player.name}", + description=move.description[:100], + value=str(move.player.id), + ) + ) super().__init__( placeholder="Select a move to remove...", min_values=1, max_values=1, - options=options + options=options, ) async def callback(self, interaction: discord.Interaction): @@ -241,27 +232,25 @@ class RemoveTradeMovesSelect(discord.ui.Select): if success: await interaction.response.send_message( - f"āœ… Removed move for player ID {player_id}", - ephemeral=True + f"Removed move for player ID {player_id}", ephemeral=True ) - # Update the embed main_view = TradeEmbedView(self.builder, interaction.user.id) embed = await create_trade_embed(self.builder) - # Edit the original message await interaction.edit_original_response(embed=embed, view=main_view) else: await interaction.response.send_message( - f"āŒ Could not remove move: {error_msg}", - ephemeral=True + f"Could not remove move: {error_msg}", ephemeral=True ) class SubmitTradeConfirmationModal(discord.ui.Modal): """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") self.builder = builder self.trade_channel = trade_channel @@ -270,7 +259,7 @@ class SubmitTradeConfirmationModal(discord.ui.Modal): label="Type 'CONFIRM' to submit for approval", placeholder="CONFIRM", required=True, - max_length=7 + max_length=7, ) self.add_item(self.confirmation) @@ -279,56 +268,52 @@ class SubmitTradeConfirmationModal(discord.ui.Modal): """Handle confirmation submission - posts acceptance view to trade channel.""" if self.confirmation.value.upper() != "CONFIRM": await interaction.response.send_message( - "āŒ Trade not submitted. You must type 'CONFIRM' exactly.", - ephemeral=True + "Trade not submitted. You must type 'CONFIRM' exactly.", + ephemeral=True, ) return await interaction.response.defer(ephemeral=True) try: - # Update trade status to PROPOSED from models.trade import TradeStatus + self.builder.trade.status = TradeStatus.PROPOSED - # Create acceptance embed and view acceptance_embed = await create_trade_acceptance_embed(self.builder) acceptance_view = TradeAcceptanceView(self.builder) - # Find the trade channel to post to channel = self.trade_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 - 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 break if channel: - # Post acceptance request to trade channel 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, - view=acceptance_view + view=acceptance_view, ) 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.", - ephemeral=True + ephemeral=True, ) else: - # No trade channel found, post in current channel 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, - view=acceptance_view + view=acceptance_view, ) except Exception as e: await interaction.followup.send( - f"āŒ Error submitting trade: {str(e)}", - ephemeral=True + f"Error submitting trade: {str(e)}", ephemeral=True ) @@ -343,8 +328,11 @@ class TradeAcceptanceView(discord.ui.View): """Get the team owned by the interacting user.""" from services.team_service import team_service from config import 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: """Check if user is a GM of a participating team.""" @@ -352,17 +340,14 @@ class TradeAcceptanceView(discord.ui.View): if not user_team: await interaction.response.send_message( - "āŒ You don't own a team in the league.", - ephemeral=True + "You don't own a team in the league.", ephemeral=True ) return False - # Check if their team (or organization) is participating participant = self.builder.trade.get_participant_by_organization(user_team) if not participant: await interaction.response.send_message( - "āŒ Your team is not part of this trade.", - ephemeral=True + "Your team is not part of this trade.", ephemeral=True ) return False @@ -374,47 +359,45 @@ class TradeAcceptanceView(discord.ui.View): if isinstance(item, discord.ui.Button): item.disabled = True - @discord.ui.button(label="Accept Trade", style=discord.ButtonStyle.success, emoji="āœ…") - async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Accept Trade", style=discord.ButtonStyle.success) + async def accept_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): """Handle accept button click.""" user_team = await self._get_user_team(interaction) if not user_team: return - # Find the participating team (could be org affiliate) participant = self.builder.trade.get_participant_by_organization(user_team) if not participant: return team_id = participant.team.id - # Check if already accepted if self.builder.has_team_accepted(team_id): await interaction.response.send_message( - f"āœ… {participant.team.abbrev} has already accepted this trade.", - ephemeral=True + f"{participant.team.abbrev} has already accepted this trade.", + ephemeral=True, ) return - # Record acceptance all_accepted = self.builder.accept_trade(team_id) if all_accepted: - # All teams accepted - finalize the trade await self._finalize_trade(interaction) else: - # Update embed to show new acceptance status embed = await create_trade_acceptance_embed(self.builder) await interaction.response.edit_message(embed=embed, view=self) - # Send confirmation to channel 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)" ) - @discord.ui.button(label="Reject Trade", style=discord.ButtonStyle.danger, emoji="āŒ") - async def reject_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Reject Trade", style=discord.ButtonStyle.danger) + async def reject_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ): """Handle reject button click - moves trade back to DRAFT.""" user_team = await self._get_user_team(interaction) if not user_team: @@ -424,20 +407,16 @@ class TradeAcceptanceView(discord.ui.View): if not participant: return - # Reject the trade self.builder.reject_trade() - # Disable buttons self.accept_button.disabled = True self.reject_button.disabled = True - # Update embed to show rejection embed = await create_trade_rejection_embed(self.builder, participant.team) await interaction.response.edit_message(embed=embed, view=self) - # Notify the channel 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"Teams can continue negotiating using `/trade` commands." ) @@ -459,41 +438,52 @@ class TradeAcceptanceView(discord.ui.View): config = get_config() - # Get next week for transactions current = await league_service.get_current_state() next_week = current.week + 1 if current else 1 - # Create FA team for reference fa_team = Team( id=config.free_agent_team_id, abbrev="FA", sname="Free Agents", lname="Free Agency", - season=self.builder.trade.season + season=self.builder.trade.season, ) # type: ignore - # Create transactions from all moves transactions: List[Transaction] = [] 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: - # Get actual team affiliates for from/to based on roster type if move.from_roster == RosterType.MAJOR_LEAGUE: old_team = move.source_team 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: - 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: old_team = move.source_team if move.to_roster == RosterType.MAJOR_LEAGUE: new_team = move.destination_team 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: - 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: new_team = move.destination_team @@ -507,18 +497,25 @@ class TradeAcceptanceView(discord.ui.View): oldteam=old_team, newteam=new_team, cancelled=False, - frozen=False # Trades are NOT frozen - immediately effective + frozen=False, ) transactions.append(transaction) - # Process supplementary moves for move in self.builder.trade.supplementary_moves: if move.from_roster == RosterType.MAJOR_LEAGUE: old_team = move.source_team 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: - 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: old_team = fa_team else: @@ -527,9 +524,17 @@ class TradeAcceptanceView(discord.ui.View): if move.to_roster == RosterType.MAJOR_LEAGUE: new_team = move.destination_team 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: - 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: new_team = fa_team else: @@ -545,45 +550,42 @@ class TradeAcceptanceView(discord.ui.View): oldteam=old_team, newteam=new_team, cancelled=False, - frozen=False # Trades are NOT frozen - immediately effective + frozen=False, ) transactions.append(transaction) - # POST transactions to database if transactions: - created_transactions = await transaction_service.create_transaction_batch(transactions) + created_transactions = ( + await transaction_service.create_transaction_batch(transactions) + ) else: created_transactions = [] - # Post to #transaction-log channel if created_transactions and interaction.client: await post_trade_to_log( bot=interaction.client, builder=self.builder, transactions=created_transactions, - effective_week=next_week + effective_week=next_week, ) - # Update trade status self.builder.trade.status = TradeStatus.ACCEPTED - # Disable buttons self.accept_button.disabled = True self.reject_button.disabled = True - # Update embed to show completion - embed = await create_trade_complete_embed(self.builder, len(created_transactions), next_week) + embed = await create_trade_complete_embed( + self.builder, len(created_transactions), next_week + ) await interaction.edit_original_response(embed=embed, view=self) - # Send completion message 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"**{len(created_transactions)} transactions** have been created for **Week {next_week}**.\n\n" f"Trade ID: `{self.builder.trade_id}`" ) - # Clear the trade builder for team in self.builder.participating_teams: clear_trade_builder_by_team(team.id) @@ -591,81 +593,79 @@ class TradeAcceptanceView(discord.ui.View): except Exception as e: await interaction.followup.send( - f"āŒ Error finalizing trade: {str(e)}", - ephemeral=True + f"Error finalizing trade: {str(e)}", ephemeral=True ) async def create_trade_acceptance_embed(builder: TradeBuilder) -> discord.Embed: """Create embed showing trade details and acceptance status.""" 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.", - color=EmbedColors.WARNING + color=EmbedColors.WARNING, ) - # Show participating teams - team_list = [f"• {team.abbrev} - {team.sname}" for team in builder.participating_teams] + team_list = [ + f"- {team.abbrev} - {team.sname}" for team in builder.participating_teams + ] embed.add_field( - name=f"šŸŸļø Participating Teams ({builder.team_count})", + name=f"Participating Teams ({builder.team_count})", value="\n".join(team_list), - inline=False + inline=False, ) - # Show cross-team moves if builder.trade.cross_team_moves: moves_text = "" 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: moves_text += f"... and {len(builder.trade.cross_team_moves) - 10} more" 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, - inline=False + inline=False, ) - # Show supplementary moves if any if builder.trade.supplementary_moves: supp_text = "" 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: supp_text += f"... and {len(builder.trade.supplementary_moves) - 5} more" embed.add_field( - name=f"āš™ļø Supplementary Moves ({len(builder.trade.supplementary_moves)})", + name=f"Supplementary Moves ({len(builder.trade.supplementary_moves)})", value=supp_text, - inline=False + inline=False, ) - # Show acceptance status status_lines = [] for team in builder.participating_teams: if team.id in builder.accepted_teams: - status_lines.append(f"āœ… **{team.abbrev}** - Accepted") + status_lines.append(f"**{team.abbrev}** - Accepted") else: - status_lines.append(f"ā³ **{team.abbrev}** - Pending") + status_lines.append(f"**{team.abbrev}** - Pending") embed.add_field( - name="šŸ“Š Acceptance Status", - value="\n".join(status_lines), - inline=False + name="Acceptance Status", value="\n".join(status_lines), inline=False ) - # Add footer - embed.set_footer(text=f"Trade ID: {builder.trade_id} • {len(builder.accepted_teams)}/{builder.team_count} teams accepted") + embed.set_footer( + text=f"Trade ID: {builder.trade_id} | {len(builder.accepted_teams)}/{builder.team_count} teams accepted" + ) 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.""" 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" - f"The trade has been moved back to **DRAFT** status.\n" - f"Teams can continue negotiating using `/trade` commands.", - color=EmbedColors.ERROR + f"The trade has been moved back to **DRAFT** status.\n" + f"Teams can continue negotiating using `/trade` commands.", + color=EmbedColors.ERROR, ) 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 -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.""" embed = EmbedTemplate.create_base_embed( - title=f"šŸŽ‰ Trade Complete! - {builder.trade.get_trade_summary()}", - description=f"All {builder.team_count} teams have accepted the trade!\n\n" - f"**{transaction_count} transactions** created for **Week {effective_week}**.", - color=EmbedColors.SUCCESS + title=f"Trade Complete - {builder.trade.get_trade_summary()}", + description=f"All {builder.team_count} teams have accepted the trade.\n\n" + f"**{transaction_count} transactions** created for **Week {effective_week}**.", + color=EmbedColors.SUCCESS, ) - # Show final acceptance status (all green) - status_lines = [f"āœ… **{team.abbrev}** - Accepted" for team in builder.participating_teams] - embed.add_field( - name="šŸ“Š Final Status", - value="\n".join(status_lines), - inline=False - ) + status_lines = [ + f"**{team.abbrev}** - Accepted" for team in builder.participating_teams + ] + embed.add_field(name="Final Status", value="\n".join(status_lines), inline=False) - # Show cross-team moves if builder.trade.cross_team_moves: moves_text = "" 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: moves_text += f"... and {len(builder.trade.cross_team_moves) - 8} more" - embed.add_field( - name=f"šŸ”„ Player Exchanges", - value=moves_text, - inline=False - ) + embed.add_field(name="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 @@ -718,7 +714,6 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed: Returns: Discord embed with current trade state """ - # Determine embed color based on trade status if builder.is_empty: color = EmbedColors.SECONDARY else: @@ -726,79 +721,79 @@ async def create_trade_embed(builder: TradeBuilder) -> discord.Embed: color = EmbedColors.SUCCESS if validation.is_legal else EmbedColors.WARNING embed = EmbedTemplate.create_base_embed( - title=f"šŸ“‹ Trade Builder - {builder.trade.get_trade_summary()}", - description=f"Build your multi-team trade", - color=color + title=f"Trade Builder - {builder.trade.get_trade_summary()}", + description="Build your multi-team trade", + color=color, ) - # Add participating teams section - team_list = [f"• {team.abbrev} - {team.sname}" for team in builder.participating_teams] + team_list = [ + f"- {team.abbrev} - {team.sname}" for team in builder.participating_teams + ] 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*", - inline=False + inline=False, ) - # Add current moves section if builder.is_empty: embed.add_field( name="Current Moves", value="*No moves yet. Use the `/trade` commands to build your trade.*", - inline=False + inline=False, ) else: - # Show cross-team moves if builder.trade.cross_team_moves: 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" if len(builder.trade.cross_team_moves) > 8: moves_text += f"... and {len(builder.trade.cross_team_moves) - 8} more" 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, - inline=False + inline=False, ) - # Show supplementary moves if builder.trade.supplementary_moves: 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" 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( - name=f"āš™ļø Supplementary Moves ({len(builder.trade.supplementary_moves)})", + name=f"Supplementary Moves ({len(builder.trade.supplementary_moves)})", value=supp_text, - inline=False + inline=False, ) - # Add quick validation summary validation = await builder.validate_trade() if validation.is_legal: - status_text = "āœ… Trade appears legal" + status_text = "Trade appears legal" else: 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( - name="šŸ” Quick Status", - value=status_text, - inline=False + 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 instructions for adding more moves - embed.add_field( - 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 + embed.set_footer( + text=f"Trade ID: {builder.trade_id} | Created: {datetime.fromisoformat(builder.trade.created_at).strftime('%H:%M:%S')}" ) - # Add footer with trade ID and timestamp - embed.set_footer(text=f"Trade ID: {builder.trade_id} • Created: {datetime.fromisoformat(builder.trade.created_at).strftime('%H:%M:%S')}") - - return embed \ No newline at end of file + return embed