Major fixes and improvements: Trade System Fixes: - Fix duplicate player moves in trade embed Player Exchanges section - Resolve "WVMiL not participating" error for Minor League destinations - Implement organizational authority model for ML/MiL/IL team relationships - Update Trade.cross_team_moves to deduplicate using moves_giving only Team Model Enhancements: - Rewrite roster_type() method using sname as definitive source per spec - Fix edge cases like "BHMIL" (Birmingham IL) vs "BHMMIL" - Update _get_base_abbrev() to use consistent sname-based logic - Add organizational lookup support in trade participation Autocomplete System: - Fix major_league_team_autocomplete invalid roster_type parameter - Implement client-side filtering using Team.roster_type() method - Add comprehensive test coverage for all autocomplete functions - Centralize autocomplete logic to shared utils functions Test Infrastructure: - Add 25 new tests for trade models and trade builder - Add 13 autocomplete function tests with error handling - Fix existing test failures with proper mocking patterns - Update dropadd tests to use shared autocomplete functions Documentation Updates: - Document trade model enhancements and deduplication fix - Add autocomplete function documentation with usage examples - Document organizational authority model and edge case handling - Update README files with recent fixes and implementation notes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
397 lines
13 KiB
Python
397 lines
13 KiB
Python
"""
|
|
Trade Commands
|
|
|
|
Interactive multi-team trade builder with real-time validation and elegant UX.
|
|
"""
|
|
from typing import Optional
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
from discord import app_commands
|
|
|
|
from utils.logging import get_contextual_logger
|
|
from utils.decorators import logged_command
|
|
from utils.autocomplete import player_autocomplete, major_league_team_autocomplete, team_autocomplete
|
|
from utils.team_utils import validate_user_has_team, get_team_by_abbrev_with_validation
|
|
from constants import SBA_CURRENT_SEASON
|
|
|
|
from services.trade_builder import (
|
|
TradeBuilder,
|
|
get_trade_builder,
|
|
clear_trade_builder
|
|
)
|
|
from services.player_service import player_service
|
|
from models.team import RosterType
|
|
from views.trade_embed import TradeEmbedView, create_trade_embed
|
|
|
|
|
|
class TradeCommands(commands.Cog):
|
|
"""Multi-team trade builder commands."""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
self.logger = get_contextual_logger(f'{__name__}.TradeCommands')
|
|
|
|
# Create the trade command group
|
|
trade_group = app_commands.Group(name="trade", description="Multi-team trade management")
|
|
|
|
@trade_group.command(
|
|
name="initiate",
|
|
description="Start a new trade with another team"
|
|
)
|
|
@app_commands.describe(
|
|
other_team="Team abbreviation to trade with"
|
|
)
|
|
@app_commands.autocomplete(other_team=major_league_team_autocomplete)
|
|
@logged_command("/trade initiate")
|
|
async def trade_initiate(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
other_team: str
|
|
):
|
|
"""Initiate a new trade with another team."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
# Get user's major league team
|
|
user_team = await validate_user_has_team(interaction)
|
|
if not user_team:
|
|
return
|
|
|
|
# Get the other team
|
|
other_team_obj = await get_team_by_abbrev_with_validation(other_team, interaction)
|
|
if not other_team_obj:
|
|
return
|
|
|
|
# Check if it's the same team
|
|
if user_team.id == other_team_obj.id:
|
|
await interaction.followup.send(
|
|
"❌ You cannot initiate a trade with yourself.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Clear any existing trade and create new one
|
|
clear_trade_builder(interaction.user.id)
|
|
trade_builder = get_trade_builder(interaction.user.id, user_team)
|
|
|
|
# Add the other team
|
|
success, error_msg = await trade_builder.add_team(other_team_obj)
|
|
if not success:
|
|
await interaction.followup.send(
|
|
f"❌ Failed to add {other_team_obj.abbrev} to trade: {error_msg}",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Show trade interface
|
|
embed = await create_trade_embed(trade_builder)
|
|
view = TradeEmbedView(trade_builder, interaction.user.id)
|
|
|
|
await interaction.followup.send(
|
|
content=f"✅ **Trade initiated between {user_team.abbrev} and {other_team_obj.abbrev}**",
|
|
embed=embed,
|
|
view=view,
|
|
ephemeral=True
|
|
)
|
|
|
|
self.logger.info(f"Trade initiated: {user_team.abbrev} ↔ {other_team_obj.abbrev}")
|
|
|
|
@trade_group.command(
|
|
name="add-team",
|
|
description="Add another team to your current trade (for 3+ team trades)"
|
|
)
|
|
@app_commands.describe(
|
|
other_team="Team abbreviation to add to the trade"
|
|
)
|
|
@app_commands.autocomplete(other_team=major_league_team_autocomplete)
|
|
@logged_command("/trade add-team")
|
|
async def trade_add_team(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
other_team: str
|
|
):
|
|
"""Add a team to an existing trade."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
# Check if user has an active trade
|
|
trade_key = f"{interaction.user.id}:trade"
|
|
from services.trade_builder import _active_trade_builders
|
|
if trade_key not in _active_trade_builders:
|
|
await interaction.followup.send(
|
|
"❌ You don't have an active trade. Use `/trade initiate` first.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
trade_builder = _active_trade_builders[trade_key]
|
|
|
|
# Get the team to add
|
|
team_to_add = await get_team_by_abbrev_with_validation(other_team, interaction)
|
|
if not team_to_add:
|
|
return
|
|
|
|
# Add the team
|
|
success, error_msg = await trade_builder.add_team(team_to_add)
|
|
if not success:
|
|
await interaction.followup.send(
|
|
f"❌ Failed to add {team_to_add.abbrev}: {error_msg}",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Show updated trade interface
|
|
embed = await create_trade_embed(trade_builder)
|
|
view = TradeEmbedView(trade_builder, interaction.user.id)
|
|
|
|
await interaction.followup.send(
|
|
content=f"✅ **Added {team_to_add.abbrev} to the trade**",
|
|
embed=embed,
|
|
view=view,
|
|
ephemeral=True
|
|
)
|
|
|
|
self.logger.info(f"Team added to trade {trade_builder.trade_id}: {team_to_add.abbrev}")
|
|
|
|
@trade_group.command(
|
|
name="add-player",
|
|
description="Add a player to the trade"
|
|
)
|
|
@app_commands.describe(
|
|
player_name="Player name; begin typing for autocomplete",
|
|
destination_team="Team abbreviation where the player will go"
|
|
)
|
|
@app_commands.autocomplete(player_name=player_autocomplete)
|
|
@app_commands.autocomplete(destination_team=team_autocomplete)
|
|
@logged_command("/trade add-player")
|
|
async def trade_add_player(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
player_name: str,
|
|
destination_team: str
|
|
):
|
|
"""Add a player move to the trade."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
# Check if user has an active trade
|
|
trade_key = f"{interaction.user.id}:trade"
|
|
from services.trade_builder import _active_trade_builders
|
|
if trade_key not in _active_trade_builders:
|
|
await interaction.followup.send(
|
|
"❌ You don't have an active trade. Use `/trade initiate` first.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
trade_builder = _active_trade_builders[trade_key]
|
|
|
|
# Get user's team
|
|
user_team = await validate_user_has_team(interaction)
|
|
if not user_team:
|
|
return
|
|
|
|
# Find the player
|
|
players = await player_service.search_players(player_name, limit=10, season=SBA_CURRENT_SEASON)
|
|
if not players:
|
|
await interaction.followup.send(
|
|
f"❌ Player '{player_name}' not found.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Use exact match if available, otherwise first result
|
|
player = None
|
|
for p in players:
|
|
if p.name.lower() == player_name.lower():
|
|
player = p
|
|
break
|
|
if not player:
|
|
player = players[0]
|
|
|
|
# Get destination team
|
|
dest_team = await get_team_by_abbrev_with_validation(destination_team, interaction)
|
|
if not dest_team:
|
|
return
|
|
|
|
# Determine source team and roster locations
|
|
# For now, assume player comes from user's team and goes to ML of destination
|
|
# TODO: More sophisticated logic to determine current roster location
|
|
from_roster = RosterType.MAJOR_LEAGUE # Default assumption
|
|
to_roster = RosterType.MAJOR_LEAGUE # Default destination
|
|
|
|
# Add the player move
|
|
success, error_msg = await trade_builder.add_player_move(
|
|
player=player,
|
|
from_team=user_team,
|
|
to_team=dest_team,
|
|
from_roster=from_roster,
|
|
to_roster=to_roster
|
|
)
|
|
|
|
if not success:
|
|
await interaction.followup.send(
|
|
f"❌ Failed to add player move: {error_msg}",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Show updated trade interface
|
|
embed = await create_trade_embed(trade_builder)
|
|
view = TradeEmbedView(trade_builder, interaction.user.id)
|
|
|
|
await interaction.followup.send(
|
|
content=f"✅ **Added {player.name}: {user_team.abbrev} → {dest_team.abbrev}**",
|
|
embed=embed,
|
|
view=view,
|
|
ephemeral=True
|
|
)
|
|
|
|
self.logger.info(f"Player added to trade {trade_builder.trade_id}: {player.name} to {dest_team.abbrev}")
|
|
|
|
@trade_group.command(
|
|
name="supplementary",
|
|
description="Add a supplementary move within your organization for roster legality"
|
|
)
|
|
@app_commands.describe(
|
|
player_name="Player name; begin typing for autocomplete",
|
|
destination="Where to move the player: Major League, Minor League, or Free Agency"
|
|
)
|
|
@app_commands.autocomplete(player_name=player_autocomplete)
|
|
@app_commands.choices(destination=[
|
|
app_commands.Choice(name="Major League", value="ml"),
|
|
app_commands.Choice(name="Minor League", value="mil"),
|
|
app_commands.Choice(name="Free Agency", value="fa")
|
|
])
|
|
@logged_command("/trade supplementary")
|
|
async def trade_supplementary(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
player_name: str,
|
|
destination: str
|
|
):
|
|
"""Add a supplementary (internal organization) move for roster legality."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
# Check if user has an active trade
|
|
trade_key = f"{interaction.user.id}:trade"
|
|
from services.trade_builder import _active_trade_builders
|
|
if trade_key not in _active_trade_builders:
|
|
await interaction.followup.send(
|
|
"❌ You don't have an active trade. Use `/trade initiate` first.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
trade_builder = _active_trade_builders[trade_key]
|
|
|
|
# Get user's team
|
|
user_team = await validate_user_has_team(interaction)
|
|
if not user_team:
|
|
return
|
|
|
|
# Find the player
|
|
players = await player_service.search_players(player_name, limit=10, season=SBA_CURRENT_SEASON)
|
|
if not players:
|
|
await interaction.followup.send(
|
|
f"❌ Player '{player_name}' not found.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
player = players[0] # Use first match
|
|
|
|
# Parse destination
|
|
destination_map = {
|
|
"ml": RosterType.MAJOR_LEAGUE,
|
|
"mil": RosterType.MINOR_LEAGUE,
|
|
"fa": RosterType.FREE_AGENCY
|
|
}
|
|
|
|
to_roster = destination_map.get(destination.lower())
|
|
if not to_roster:
|
|
await interaction.followup.send(
|
|
f"❌ Invalid destination: {destination}",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Determine current roster (default assumption)
|
|
from_roster = RosterType.MINOR_LEAGUE if to_roster == RosterType.MAJOR_LEAGUE else RosterType.MAJOR_LEAGUE
|
|
|
|
# Add supplementary move
|
|
success, error_msg = await trade_builder.add_supplementary_move(
|
|
team=user_team,
|
|
player=player,
|
|
from_roster=from_roster,
|
|
to_roster=to_roster
|
|
)
|
|
|
|
if not success:
|
|
await interaction.followup.send(
|
|
f"❌ Failed to add supplementary move: {error_msg}",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Show updated trade interface
|
|
embed = await create_trade_embed(trade_builder)
|
|
view = TradeEmbedView(trade_builder, interaction.user.id)
|
|
|
|
await interaction.followup.send(
|
|
content=f"✅ **Added supplementary move: {player.name} → {destination.upper()}**",
|
|
embed=embed,
|
|
view=view,
|
|
ephemeral=True
|
|
)
|
|
|
|
self.logger.info(f"Supplementary move added to trade {trade_builder.trade_id}: {player.name} to {destination}")
|
|
|
|
@trade_group.command(
|
|
name="view",
|
|
description="View your current trade"
|
|
)
|
|
@logged_command("/trade view")
|
|
async def trade_view(self, interaction: discord.Interaction):
|
|
"""View the current trade."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
trade_key = f"{interaction.user.id}:trade"
|
|
from services.trade_builder import _active_trade_builders
|
|
if trade_key not in _active_trade_builders:
|
|
await interaction.followup.send(
|
|
"❌ You don't have an active trade.",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
trade_builder = _active_trade_builders[trade_key]
|
|
|
|
# Show trade interface
|
|
embed = await create_trade_embed(trade_builder)
|
|
view = TradeEmbedView(trade_builder, interaction.user.id)
|
|
|
|
await interaction.followup.send(
|
|
embed=embed,
|
|
view=view,
|
|
ephemeral=True
|
|
)
|
|
|
|
@trade_group.command(
|
|
name="clear",
|
|
description="Clear your current trade"
|
|
)
|
|
@logged_command("/trade clear")
|
|
async def trade_clear(self, interaction: discord.Interaction):
|
|
"""Clear the current trade."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
clear_trade_builder(interaction.user.id)
|
|
|
|
await interaction.followup.send(
|
|
"✅ Your trade has been cleared.",
|
|
ephemeral=True
|
|
)
|
|
|
|
|
|
async def setup(bot):
|
|
"""Setup function for the cog."""
|
|
await bot.add_cog(TradeCommands(bot)) |