CLAUDE: Add pagination and move ID display to /mymoves command
- Fix transaction count bug by filtering transactions >= current week - Add TransactionPaginationView with "Show Move IDs" button - Implement intelligent message chunking for long transaction lists - Remove emojis from individual transaction lines (kept in headers) - Display 10 transactions per page with navigation buttons - Show move IDs on demand via ephemeral button (stays under 2000 char limit) - Update all tests to validate pagination and chunking behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b61cad2478
commit
3c66ada99d
@ -12,7 +12,9 @@ from discord import app_commands
|
|||||||
|
|
||||||
from utils.logging import get_contextual_logger
|
from utils.logging import get_contextual_logger
|
||||||
from utils.decorators import logged_command
|
from utils.decorators import logged_command
|
||||||
|
from utils.team_utils import get_user_major_league_team
|
||||||
from views.embeds import EmbedColors, EmbedTemplate
|
from views.embeds import EmbedColors, EmbedTemplate
|
||||||
|
from views.base import PaginationView
|
||||||
from constants import SBA_CURRENT_SEASON
|
from constants import SBA_CURRENT_SEASON
|
||||||
|
|
||||||
from services.transaction_service import transaction_service
|
from services.transaction_service import transaction_service
|
||||||
@ -21,6 +23,81 @@ from services.team_service import team_service
|
|||||||
# No longer need TransactionStatus enum
|
# No longer need TransactionStatus enum
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionPaginationView(PaginationView):
|
||||||
|
"""Custom pagination view with Show Move IDs button."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
pages: list[discord.Embed],
|
||||||
|
all_transactions: list,
|
||||||
|
user_id: int,
|
||||||
|
timeout: float = 300.0,
|
||||||
|
show_page_numbers: bool = True
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
pages=pages,
|
||||||
|
user_id=user_id,
|
||||||
|
timeout=timeout,
|
||||||
|
show_page_numbers=show_page_numbers
|
||||||
|
)
|
||||||
|
self.all_transactions = all_transactions
|
||||||
|
|
||||||
|
@discord.ui.button(label="Show Move IDs", style=discord.ButtonStyle.secondary, emoji="🔍", row=1)
|
||||||
|
async def show_move_ids(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
"""Show all move IDs in an ephemeral message."""
|
||||||
|
self.increment_interaction_count()
|
||||||
|
|
||||||
|
if not self.all_transactions:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"No transactions to show.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build the move ID list
|
||||||
|
header = "📋 **Move IDs for Your Transactions**\n"
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for transaction in self.all_transactions:
|
||||||
|
lines.append(
|
||||||
|
f"• Week {transaction.week}: {transaction.player.name} → `{transaction.moveid}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Discord has a 2000 character limit for messages
|
||||||
|
# Chunk messages to stay under the limit
|
||||||
|
messages = []
|
||||||
|
current_message = header
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# Check if adding this line would exceed limit (leave 50 char buffer)
|
||||||
|
if len(current_message) + len(line) + 1 > 1950:
|
||||||
|
messages.append(current_message)
|
||||||
|
current_message = line + "\n"
|
||||||
|
else:
|
||||||
|
current_message += line + "\n"
|
||||||
|
|
||||||
|
# Add the last message if it has content beyond the header
|
||||||
|
if current_message.strip() != header.strip():
|
||||||
|
messages.append(current_message)
|
||||||
|
|
||||||
|
# Send the messages
|
||||||
|
if not messages:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"No transactions to display.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Send first message as response
|
||||||
|
await interaction.response.send_message(messages[0], ephemeral=True)
|
||||||
|
|
||||||
|
# Send remaining messages as followups
|
||||||
|
if len(messages) > 1:
|
||||||
|
for msg in messages[1:]:
|
||||||
|
await interaction.followup.send(msg, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
class TransactionCommands(commands.Cog):
|
class TransactionCommands(commands.Cog):
|
||||||
"""Transaction command handlers for roster management."""
|
"""Transaction command handlers for roster management."""
|
||||||
|
|
||||||
@ -45,17 +122,15 @@ class TransactionCommands(commands.Cog):
|
|||||||
await interaction.response.defer()
|
await interaction.response.defer()
|
||||||
|
|
||||||
# Get user's team
|
# Get user's team
|
||||||
user_teams = await team_service.get_teams_by_owner(interaction.user.id, SBA_CURRENT_SEASON)
|
team = await get_user_major_league_team(interaction.user.id, SBA_CURRENT_SEASON)
|
||||||
|
|
||||||
if not user_teams:
|
if not team:
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
"❌ You don't appear to own a team in the current season.",
|
"❌ You don't appear to own a team in the current season.",
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
team = user_teams[0] # Use first team if multiple
|
|
||||||
|
|
||||||
# Get transactions in parallel
|
# Get transactions in parallel
|
||||||
pending_task = transaction_service.get_pending_transactions(team.abbrev, SBA_CURRENT_SEASON)
|
pending_task = transaction_service.get_pending_transactions(team.abbrev, SBA_CURRENT_SEASON)
|
||||||
frozen_task = transaction_service.get_frozen_transactions(team.abbrev, SBA_CURRENT_SEASON)
|
frozen_task = transaction_service.get_frozen_transactions(team.abbrev, SBA_CURRENT_SEASON)
|
||||||
@ -69,20 +144,40 @@ class TransactionCommands(commands.Cog):
|
|||||||
cancelled_transactions = []
|
cancelled_transactions = []
|
||||||
if show_cancelled:
|
if show_cancelled:
|
||||||
cancelled_transactions = await transaction_service.get_team_transactions(
|
cancelled_transactions = await transaction_service.get_team_transactions(
|
||||||
team.abbrev,
|
team.abbrev,
|
||||||
SBA_CURRENT_SEASON,
|
SBA_CURRENT_SEASON,
|
||||||
cancelled=True
|
cancelled=True
|
||||||
)
|
)
|
||||||
|
|
||||||
embed = await self._create_my_moves_embed(
|
pages = self._create_my_moves_pages(
|
||||||
team,
|
team,
|
||||||
pending_transactions,
|
pending_transactions,
|
||||||
frozen_transactions,
|
frozen_transactions,
|
||||||
processed_transactions,
|
processed_transactions,
|
||||||
cancelled_transactions
|
cancelled_transactions
|
||||||
)
|
)
|
||||||
|
|
||||||
await interaction.followup.send(embed=embed)
|
# Collect all transactions for the "Show Move IDs" button
|
||||||
|
all_transactions = (
|
||||||
|
pending_transactions +
|
||||||
|
frozen_transactions +
|
||||||
|
processed_transactions +
|
||||||
|
cancelled_transactions
|
||||||
|
)
|
||||||
|
|
||||||
|
# If only one page and no transactions, send without any buttons
|
||||||
|
if len(pages) == 1 and not all_transactions:
|
||||||
|
await interaction.followup.send(embed=pages[0])
|
||||||
|
else:
|
||||||
|
# Use custom pagination view with "Show Move IDs" button
|
||||||
|
view = TransactionPaginationView(
|
||||||
|
pages=pages,
|
||||||
|
all_transactions=all_transactions,
|
||||||
|
user_id=interaction.user.id,
|
||||||
|
timeout=300.0,
|
||||||
|
show_page_numbers=True
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=view.get_current_embed(), view=view)
|
||||||
|
|
||||||
@app_commands.command(
|
@app_commands.command(
|
||||||
name="legal",
|
name="legal",
|
||||||
@ -159,106 +254,166 @@ class TransactionCommands(commands.Cog):
|
|||||||
|
|
||||||
await interaction.followup.send(embed=embed)
|
await interaction.followup.send(embed=embed)
|
||||||
|
|
||||||
async def _create_my_moves_embed(
|
def _create_my_moves_pages(
|
||||||
self,
|
self,
|
||||||
team,
|
team,
|
||||||
pending_transactions,
|
pending_transactions,
|
||||||
frozen_transactions,
|
frozen_transactions,
|
||||||
processed_transactions,
|
processed_transactions,
|
||||||
cancelled_transactions
|
cancelled_transactions
|
||||||
) -> discord.Embed:
|
) -> list[discord.Embed]:
|
||||||
"""Create embed showing user's transaction status."""
|
"""Create paginated embeds showing user's transaction status."""
|
||||||
|
|
||||||
embed = EmbedTemplate.create_base_embed(
|
pages = []
|
||||||
title=f"📋 Transaction Status - {team.abbrev}",
|
transactions_per_page = 10
|
||||||
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
|
||||||
color=EmbedColors.INFO
|
# Helper function to create transaction lines without emojis
|
||||||
)
|
def format_transaction(transaction):
|
||||||
|
return f"Week {transaction.week}: {transaction.move_description}"
|
||||||
# Add team thumbnail if available
|
|
||||||
if hasattr(team, 'thumbnail') and team.thumbnail:
|
# Page 1: Summary + Pending Transactions
|
||||||
embed.set_thumbnail(url=team.thumbnail)
|
|
||||||
|
|
||||||
# Pending transactions
|
|
||||||
if pending_transactions:
|
if pending_transactions:
|
||||||
pending_lines = []
|
total_pending = len(pending_transactions)
|
||||||
for transaction in pending_transactions[-5:]: # Show last 5
|
total_pages = (total_pending + transactions_per_page - 1) // transactions_per_page
|
||||||
pending_lines.append(
|
|
||||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
for page_num in range(total_pages):
|
||||||
|
start_idx = page_num * transactions_per_page
|
||||||
|
end_idx = min(start_idx + transactions_per_page, total_pending)
|
||||||
|
page_transactions = pending_transactions[start_idx:end_idx]
|
||||||
|
|
||||||
|
embed = EmbedTemplate.create_base_embed(
|
||||||
|
title=f"📋 Transaction Status - {team.abbrev}",
|
||||||
|
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||||
|
color=EmbedColors.INFO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add team thumbnail if available
|
||||||
|
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||||
|
embed.set_thumbnail(url=team.thumbnail)
|
||||||
|
|
||||||
|
# Pending transactions for this page
|
||||||
|
pending_lines = [format_transaction(tx) for tx in page_transactions]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"⏳ Pending Transactions ({total_pending} total)",
|
||||||
|
value="\n".join(pending_lines),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add summary only on first page
|
||||||
|
if page_num == 0:
|
||||||
|
total_frozen = len(frozen_transactions)
|
||||||
|
status_text = []
|
||||||
|
if total_pending > 0:
|
||||||
|
status_text.append(f"{total_pending} pending")
|
||||||
|
if total_frozen > 0:
|
||||||
|
status_text.append(f"{total_frozen} scheduled")
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="Summary",
|
||||||
|
value=", ".join(status_text) if status_text else "No active transactions",
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
pages.append(embed)
|
||||||
|
else:
|
||||||
|
# No pending transactions - create single page
|
||||||
|
embed = EmbedTemplate.create_base_embed(
|
||||||
|
title=f"📋 Transaction Status - {team.abbrev}",
|
||||||
|
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||||
|
color=EmbedColors.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||||
|
embed.set_thumbnail(url=team.thumbnail)
|
||||||
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="⏳ Pending Transactions",
|
name="⏳ Pending Transactions",
|
||||||
value="\n".join(pending_lines),
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
embed.add_field(
|
|
||||||
name="⏳ Pending Transactions",
|
|
||||||
value="No pending transactions",
|
value="No pending transactions",
|
||||||
inline=False
|
inline=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Frozen transactions (scheduled for processing)
|
total_frozen = len(frozen_transactions)
|
||||||
if frozen_transactions:
|
status_text = []
|
||||||
frozen_lines = []
|
if total_frozen > 0:
|
||||||
for transaction in frozen_transactions[-3:]: # Show last 3
|
status_text.append(f"{total_frozen} scheduled")
|
||||||
frozen_lines.append(
|
|
||||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="❄️ Scheduled for Processing",
|
name="Summary",
|
||||||
|
value=", ".join(status_text) if status_text else "No active transactions",
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
pages.append(embed)
|
||||||
|
|
||||||
|
# Additional page: Frozen transactions
|
||||||
|
if frozen_transactions:
|
||||||
|
embed = EmbedTemplate.create_base_embed(
|
||||||
|
title=f"📋 Transaction Status - {team.abbrev}",
|
||||||
|
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||||
|
color=EmbedColors.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||||
|
embed.set_thumbnail(url=team.thumbnail)
|
||||||
|
|
||||||
|
frozen_lines = [format_transaction(tx) for tx in frozen_transactions]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"❄️ Scheduled for Processing ({len(frozen_transactions)} total)",
|
||||||
value="\n".join(frozen_lines),
|
value="\n".join(frozen_lines),
|
||||||
inline=False
|
inline=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Recent processed transactions
|
pages.append(embed)
|
||||||
|
|
||||||
|
# Additional page: Recently processed transactions
|
||||||
if processed_transactions:
|
if processed_transactions:
|
||||||
processed_lines = []
|
embed = EmbedTemplate.create_base_embed(
|
||||||
for transaction in processed_transactions[-3:]: # Show last 3
|
title=f"📋 Transaction Status - {team.abbrev}",
|
||||||
processed_lines.append(
|
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
color=EmbedColors.INFO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||||
|
embed.set_thumbnail(url=team.thumbnail)
|
||||||
|
|
||||||
|
processed_lines = [format_transaction(tx) for tx in processed_transactions[-20:]] # Last 20
|
||||||
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="✅ Recently Processed",
|
name=f"✅ Recently Processed ({len(processed_transactions[-20:])} shown)",
|
||||||
value="\n".join(processed_lines),
|
value="\n".join(processed_lines),
|
||||||
inline=False
|
inline=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cancelled transactions (if requested)
|
pages.append(embed)
|
||||||
|
|
||||||
|
# Additional page: Cancelled transactions (if requested)
|
||||||
if cancelled_transactions:
|
if cancelled_transactions:
|
||||||
cancelled_lines = []
|
embed = EmbedTemplate.create_base_embed(
|
||||||
for transaction in cancelled_transactions[-2:]: # Show last 2
|
title=f"📋 Transaction Status - {team.abbrev}",
|
||||||
cancelled_lines.append(
|
description=f"{team.lname} • Season {SBA_CURRENT_SEASON}",
|
||||||
f"{transaction.status_emoji} Week {transaction.week}: {transaction.move_description}"
|
color=EmbedColors.INFO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if hasattr(team, 'thumbnail') and team.thumbnail:
|
||||||
|
embed.set_thumbnail(url=team.thumbnail)
|
||||||
|
|
||||||
|
cancelled_lines = [format_transaction(tx) for tx in cancelled_transactions[-20:]] # Last 20
|
||||||
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="❌ Cancelled Transactions",
|
name=f"❌ Cancelled Transactions ({len(cancelled_transactions[-20:])} shown)",
|
||||||
value="\n".join(cancelled_lines),
|
value="\n".join(cancelled_lines),
|
||||||
inline=False
|
inline=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Transaction summary
|
pages.append(embed)
|
||||||
total_pending = len(pending_transactions)
|
|
||||||
total_frozen = len(frozen_transactions)
|
# Add footer to all pages
|
||||||
|
for page in pages:
|
||||||
status_text = []
|
page.set_footer(text="Use /legal to check roster legality")
|
||||||
if total_pending > 0:
|
|
||||||
status_text.append(f"{total_pending} pending")
|
return pages
|
||||||
if total_frozen > 0:
|
|
||||||
status_text.append(f"{total_frozen} scheduled")
|
|
||||||
|
|
||||||
embed.add_field(
|
|
||||||
name="Summary",
|
|
||||||
value=", ".join(status_text) if status_text else "No active transactions",
|
|
||||||
inline=True
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.set_footer(text="Use /legal to check roster legality")
|
|
||||||
return embed
|
|
||||||
|
|
||||||
async def _create_legal_embed(
|
async def _create_legal_embed(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -77,13 +77,30 @@ class TransactionService(BaseService[Transaction]):
|
|||||||
raise APIException(f"Failed to retrieve transactions: {e}")
|
raise APIException(f"Failed to retrieve transactions: {e}")
|
||||||
|
|
||||||
async def get_pending_transactions(self, team_abbrev: str, season: int) -> List[Transaction]:
|
async def get_pending_transactions(self, team_abbrev: str, season: int) -> List[Transaction]:
|
||||||
"""Get pending transactions for a team."""
|
"""Get pending (future) transactions for a team."""
|
||||||
return await self.get_team_transactions(
|
try:
|
||||||
team_abbrev,
|
# Get current week to filter future transactions
|
||||||
season,
|
current_data = await self.get_client()
|
||||||
cancelled=False,
|
current_response = await current_data.get('current')
|
||||||
frozen=False
|
current_week = current_response.get('week', 0) if current_response else 0
|
||||||
)
|
|
||||||
|
# Get transactions from current week onward
|
||||||
|
return await self.get_team_transactions(
|
||||||
|
team_abbrev,
|
||||||
|
season,
|
||||||
|
cancelled=False,
|
||||||
|
frozen=False,
|
||||||
|
week_start=current_week
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not get current week, returning all non-cancelled/non-frozen transactions: {e}")
|
||||||
|
# Fallback to all non-cancelled/non-frozen if we can't get current week
|
||||||
|
return await self.get_team_transactions(
|
||||||
|
team_abbrev,
|
||||||
|
season,
|
||||||
|
cancelled=False,
|
||||||
|
frozen=False
|
||||||
|
)
|
||||||
|
|
||||||
async def get_frozen_transactions(self, team_abbrev: str, season: int) -> List[Transaction]:
|
async def get_frozen_transactions(self, team_abbrev: str, season: int) -> List[Transaction]:
|
||||||
"""Get frozen (scheduled for processing) transactions for a team."""
|
"""Get frozen (scheduled for processing) transactions for a team."""
|
||||||
|
|||||||
@ -113,31 +113,28 @@ class TestTransactionCommands:
|
|||||||
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
||||||
frozen_tx = [tx for tx in mock_transactions if tx.is_frozen]
|
frozen_tx = [tx for tx in mock_transactions if tx.is_frozen]
|
||||||
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
||||||
|
|
||||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
with patch('utils.team_utils.team_service') as mock_team_utils_service:
|
||||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||||
|
|
||||||
# Mock service responses
|
# Mock service responses
|
||||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
mock_team_utils_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_tx)
|
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_tx)
|
||||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=frozen_tx)
|
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=frozen_tx)
|
||||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Execute command
|
# Execute command
|
||||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
||||||
|
|
||||||
# Verify interaction flow
|
# Verify interaction flow
|
||||||
mock_interaction.response.defer.assert_called_once()
|
mock_interaction.response.defer.assert_called_once()
|
||||||
mock_interaction.followup.send.assert_called_once()
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
|
||||||
# Verify service calls
|
# Verify service calls
|
||||||
mock_team_service.get_teams_by_owner.assert_called_once_with(
|
|
||||||
mock_interaction.user.id, 12
|
|
||||||
)
|
|
||||||
mock_tx_service.get_pending_transactions.assert_called_once_with('WV', 12)
|
mock_tx_service.get_pending_transactions.assert_called_once_with('WV', 12)
|
||||||
mock_tx_service.get_frozen_transactions.assert_called_once_with('WV', 12)
|
mock_tx_service.get_frozen_transactions.assert_called_once_with('WV', 12)
|
||||||
mock_tx_service.get_processed_transactions.assert_called_once_with('WV', 12)
|
mock_tx_service.get_processed_transactions.assert_called_once_with('WV', 12)
|
||||||
|
|
||||||
# Check embed was sent
|
# Check embed was sent
|
||||||
embed_call = mock_interaction.followup.send.call_args
|
embed_call = mock_interaction.followup.send.call_args
|
||||||
assert 'embed' in embed_call.kwargs
|
assert 'embed' in embed_call.kwargs
|
||||||
@ -146,18 +143,18 @@ class TestTransactionCommands:
|
|||||||
async def test_my_moves_with_cancelled(self, commands_cog, mock_interaction, mock_team, mock_transactions):
|
async def test_my_moves_with_cancelled(self, commands_cog, mock_interaction, mock_team, mock_transactions):
|
||||||
"""Test /mymoves command with cancelled transactions shown."""
|
"""Test /mymoves command with cancelled transactions shown."""
|
||||||
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
||||||
|
|
||||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||||
|
|
||||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_pending_transactions = AsyncMock(return_value=[])
|
||||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||||
mock_tx_service.get_team_transactions = AsyncMock(return_value=cancelled_tx)
|
mock_tx_service.get_team_transactions = AsyncMock(return_value=cancelled_tx)
|
||||||
|
|
||||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=True)
|
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=True)
|
||||||
|
|
||||||
# Verify cancelled transactions were requested
|
# Verify cancelled transactions were requested
|
||||||
mock_tx_service.get_team_transactions.assert_called_once_with(
|
mock_tx_service.get_team_transactions.assert_called_once_with(
|
||||||
'WV', 12, cancelled=True
|
'WV', 12, cancelled=True
|
||||||
@ -166,11 +163,11 @@ class TestTransactionCommands:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_my_moves_no_team(self, commands_cog, mock_interaction):
|
async def test_my_moves_no_team(self, commands_cog, mock_interaction):
|
||||||
"""Test /mymoves command when user has no team."""
|
"""Test /mymoves command when user has no team."""
|
||||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[])
|
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[])
|
||||||
|
|
||||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
|
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
|
||||||
|
|
||||||
# Should send error message
|
# Should send error message
|
||||||
mock_interaction.followup.send.assert_called_once()
|
mock_interaction.followup.send.assert_called_once()
|
||||||
call_args = mock_interaction.followup.send.call_args
|
call_args = mock_interaction.followup.send.call_args
|
||||||
@ -180,12 +177,12 @@ class TestTransactionCommands:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_my_moves_api_error(self, commands_cog, mock_interaction, mock_team):
|
async def test_my_moves_api_error(self, commands_cog, mock_interaction, mock_team):
|
||||||
"""Test /mymoves command with API error."""
|
"""Test /mymoves command with API error."""
|
||||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||||
|
|
||||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||||
mock_tx_service.get_pending_transactions.side_effect = APIException("API Error")
|
mock_tx_service.get_pending_transactions.side_effect = APIException("API Error")
|
||||||
|
|
||||||
# Should raise the exception (logged_command decorator handles it)
|
# Should raise the exception (logged_command decorator handles it)
|
||||||
with pytest.raises(APIException):
|
with pytest.raises(APIException):
|
||||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
|
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
|
||||||
@ -308,45 +305,167 @@ class TestTransactionCommands:
|
|||||||
assert "Could not retrieve roster data" in call_args.args[0]
|
assert "Could not retrieve roster data" in call_args.args[0]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_my_moves_embed(self, commands_cog, mock_team, mock_transactions):
|
async def test_create_my_moves_pages(self, commands_cog, mock_team, mock_transactions):
|
||||||
"""Test embed creation for /mymoves command."""
|
"""Test paginated embed creation for /mymoves command."""
|
||||||
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
||||||
frozen_tx = [tx for tx in mock_transactions if tx.is_frozen]
|
frozen_tx = [tx for tx in mock_transactions if tx.is_frozen]
|
||||||
processed_tx = []
|
processed_tx = []
|
||||||
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
cancelled_tx = [tx for tx in mock_transactions if tx.is_cancelled]
|
||||||
|
|
||||||
embed = await commands_cog._create_my_moves_embed(
|
pages = commands_cog._create_my_moves_pages(
|
||||||
mock_team, pending_tx, frozen_tx, processed_tx, cancelled_tx
|
mock_team, pending_tx, frozen_tx, processed_tx, cancelled_tx
|
||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(embed, discord.Embed)
|
assert len(pages) > 0
|
||||||
assert embed.title == "📋 Transaction Status - WV"
|
first_page = pages[0]
|
||||||
assert "West Virginia Black Bears • Season 12" in embed.description
|
assert isinstance(first_page, discord.Embed)
|
||||||
|
assert first_page.title == "📋 Transaction Status - WV"
|
||||||
# Check that fields are created for each transaction type
|
assert "West Virginia Black Bears • Season 12" in first_page.description
|
||||||
field_names = [field.name for field in embed.fields]
|
|
||||||
assert "⏳ Pending Transactions" in field_names
|
# Check that fields are created for transaction types
|
||||||
assert "❄️ Scheduled for Processing" in field_names
|
field_names = [field.name for field in first_page.fields]
|
||||||
assert "❌ Cancelled Transactions" in field_names
|
assert any("Pending Transactions" in name for name in field_names)
|
||||||
assert "Summary" in field_names
|
|
||||||
|
|
||||||
# Verify thumbnail is set
|
# Verify thumbnail is set
|
||||||
assert embed.thumbnail.url == mock_team.thumbnail
|
assert first_page.thumbnail.url == mock_team.thumbnail
|
||||||
|
|
||||||
|
# Verify emoji is NOT in individual transaction lines
|
||||||
|
for page in pages:
|
||||||
|
for field in page.fields:
|
||||||
|
if "Pending" in field.name or "Scheduled" in field.name or "Cancelled" in field.name:
|
||||||
|
# Check that emojis (⏳, ❄️, ❌) are NOT in the field value
|
||||||
|
assert "⏳" not in field.value
|
||||||
|
assert "❄️" not in field.value
|
||||||
|
assert "✅" not in field.value
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_my_moves_embed_no_transactions(self, commands_cog, mock_team):
|
async def test_create_my_moves_pages_no_transactions(self, commands_cog, mock_team):
|
||||||
"""Test embed creation with no transactions."""
|
"""Test paginated embed creation with no transactions."""
|
||||||
embed = await commands_cog._create_my_moves_embed(
|
pages = commands_cog._create_my_moves_pages(
|
||||||
mock_team, [], [], [], []
|
mock_team, [], [], [], []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert len(pages) == 1 # Should have single page
|
||||||
|
embed = pages[0]
|
||||||
|
|
||||||
# Find the pending transactions field
|
# Find the pending transactions field
|
||||||
pending_field = next(f for f in embed.fields if "Pending" in f.name)
|
pending_field = next(f for f in embed.fields if "Pending" in f.name)
|
||||||
assert pending_field.value == "No pending transactions"
|
assert pending_field.value == "No pending transactions"
|
||||||
|
|
||||||
# Summary should show no active transactions
|
# Summary should show no active transactions
|
||||||
summary_field = next(f for f in embed.fields if f.name == "Summary")
|
summary_field = next(f for f in embed.fields if f.name == "Summary")
|
||||||
assert summary_field.value == "No active transactions"
|
assert summary_field.value == "No active transactions"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_transaction_pagination_view_with_move_ids(self, commands_cog, mock_interaction, mock_team, mock_transactions):
|
||||||
|
"""Test that TransactionPaginationView is created with move IDs button."""
|
||||||
|
from commands.transactions.management import TransactionPaginationView
|
||||||
|
|
||||||
|
pending_tx = [tx for tx in mock_transactions if tx.is_pending]
|
||||||
|
|
||||||
|
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||||
|
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||||
|
|
||||||
|
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||||
|
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_tx)
|
||||||
|
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||||
|
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
||||||
|
|
||||||
|
# Verify TransactionPaginationView was created
|
||||||
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
call_args = mock_interaction.followup.send.call_args
|
||||||
|
view = call_args.kwargs.get('view')
|
||||||
|
|
||||||
|
assert view is not None
|
||||||
|
assert isinstance(view, TransactionPaginationView)
|
||||||
|
assert len(view.all_transactions) == len(pending_tx)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_show_move_ids_handles_long_lists(self, mock_team, mock_transactions):
|
||||||
|
"""Test that Show Move IDs button properly chunks very long transaction lists."""
|
||||||
|
from commands.transactions.management import TransactionPaginationView
|
||||||
|
|
||||||
|
# Create 100 transactions to simulate a very long list
|
||||||
|
base_data = {
|
||||||
|
'season': 12,
|
||||||
|
'player': {
|
||||||
|
'id': 12472,
|
||||||
|
'name': 'Very Long Player Name That Takes Up Space',
|
||||||
|
'wara': 2.47,
|
||||||
|
'season': 12,
|
||||||
|
'pos_1': 'LF'
|
||||||
|
},
|
||||||
|
'oldteam': {
|
||||||
|
'id': 508,
|
||||||
|
'abbrev': 'NYD',
|
||||||
|
'sname': 'Diamonds',
|
||||||
|
'lname': 'New York Diamonds',
|
||||||
|
'season': 12
|
||||||
|
},
|
||||||
|
'newteam': {
|
||||||
|
'id': 499,
|
||||||
|
'abbrev': 'WV',
|
||||||
|
'sname': 'Black Bears',
|
||||||
|
'lname': 'West Virginia Black Bears',
|
||||||
|
'season': 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
many_transactions = []
|
||||||
|
for i in range(100):
|
||||||
|
tx_data = {
|
||||||
|
**base_data,
|
||||||
|
'id': i,
|
||||||
|
'week': 10 + (i % 5),
|
||||||
|
'moveid': f'Season-012-Week-{10 + (i % 5)}-Move-{i:03d}',
|
||||||
|
'cancelled': False,
|
||||||
|
'frozen': False
|
||||||
|
}
|
||||||
|
many_transactions.append(Transaction.from_api_data(tx_data))
|
||||||
|
|
||||||
|
# Create view with many transactions
|
||||||
|
pages = [discord.Embed(title="Test")]
|
||||||
|
view = TransactionPaginationView(
|
||||||
|
pages=pages,
|
||||||
|
all_transactions=many_transactions,
|
||||||
|
user_id=258104532423147520,
|
||||||
|
timeout=300.0,
|
||||||
|
show_page_numbers=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create mock interaction
|
||||||
|
mock_interaction = AsyncMock()
|
||||||
|
mock_button = MagicMock()
|
||||||
|
|
||||||
|
# Find the show_move_ids button and call its callback directly
|
||||||
|
show_move_ids_button = None
|
||||||
|
for item in view.children:
|
||||||
|
if hasattr(item, 'label') and item.label == "Show Move IDs":
|
||||||
|
show_move_ids_button = item
|
||||||
|
break
|
||||||
|
|
||||||
|
assert show_move_ids_button is not None, "Show Move IDs button not found"
|
||||||
|
|
||||||
|
# Call the button's callback
|
||||||
|
await show_move_ids_button.callback(mock_interaction)
|
||||||
|
|
||||||
|
# Verify response was sent
|
||||||
|
mock_interaction.response.send_message.assert_called_once()
|
||||||
|
|
||||||
|
# Get the message that was sent
|
||||||
|
call_args = mock_interaction.response.send_message.call_args
|
||||||
|
first_message = call_args.args[0]
|
||||||
|
|
||||||
|
# Verify first message is under 2000 characters
|
||||||
|
assert len(first_message) < 2000, f"First message is {len(first_message)} characters (exceeds 2000 limit)"
|
||||||
|
|
||||||
|
# If there were followup messages, verify they're also under 2000 chars
|
||||||
|
if mock_interaction.followup.send.called:
|
||||||
|
for call in mock_interaction.followup.send.call_args_list:
|
||||||
|
followup_message = call.args[0]
|
||||||
|
assert len(followup_message) < 2000, f"Followup message is {len(followup_message)} characters (exceeds 2000 limit)"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_legal_embed_all_legal(self, commands_cog, mock_team):
|
async def test_create_legal_embed_all_legal(self, commands_cog, mock_team):
|
||||||
@ -470,7 +589,7 @@ class TestTransactionCommandsIntegration:
|
|||||||
"""Test complete /mymoves workflow with realistic data volumes."""
|
"""Test complete /mymoves workflow with realistic data volumes."""
|
||||||
mock_interaction = AsyncMock()
|
mock_interaction = AsyncMock()
|
||||||
mock_interaction.user.id = 258104532423147520
|
mock_interaction.user.id = 258104532423147520
|
||||||
|
|
||||||
# Create realistic transaction volumes
|
# Create realistic transaction volumes
|
||||||
pending_transactions = []
|
pending_transactions = []
|
||||||
for i in range(15): # 15 pending transactions
|
for i in range(15): # 15 pending transactions
|
||||||
@ -486,7 +605,7 @@ class TestTransactionCommandsIntegration:
|
|||||||
'frozen': False
|
'frozen': False
|
||||||
}
|
}
|
||||||
pending_transactions.append(Transaction.from_api_data(tx_data))
|
pending_transactions.append(Transaction.from_api_data(tx_data))
|
||||||
|
|
||||||
mock_team = Team.from_api_data({
|
mock_team = Team.from_api_data({
|
||||||
'id': 499,
|
'id': 499,
|
||||||
'abbrev': 'WV',
|
'abbrev': 'WV',
|
||||||
@ -494,43 +613,50 @@ class TestTransactionCommandsIntegration:
|
|||||||
'lname': 'West Virginia Black Bears',
|
'lname': 'West Virginia Black Bears',
|
||||||
'season': 12
|
'season': 12
|
||||||
})
|
})
|
||||||
|
|
||||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||||
|
|
||||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_transactions)
|
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_transactions)
|
||||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
|
||||||
|
|
||||||
# Verify embed was created and sent
|
# Verify embed was created and sent
|
||||||
mock_interaction.followup.send.assert_called_once()
|
mock_interaction.followup.send.assert_called_once()
|
||||||
embed_call = mock_interaction.followup.send.call_args
|
embed_call = mock_interaction.followup.send.call_args
|
||||||
embed = embed_call.kwargs['embed']
|
embed = embed_call.kwargs['embed']
|
||||||
|
|
||||||
# Check that only last 5 pending transactions are shown
|
# With 15 transactions, should show 10 per page
|
||||||
pending_field = next(f for f in embed.fields if "Pending" in f.name)
|
pending_field = next(f for f in embed.fields if "Pending" in f.name)
|
||||||
lines = pending_field.value.split('\n')
|
lines = pending_field.value.split('\n')
|
||||||
assert len(lines) == 5 # Should show only last 5
|
assert len(lines) == 10 # Should show 10 per page
|
||||||
|
|
||||||
# Verify summary shows correct count
|
# Verify summary shows correct count
|
||||||
summary_field = next(f for f in embed.fields if f.name == "Summary")
|
summary_field = next(f for f in embed.fields if f.name == "Summary")
|
||||||
assert "15 pending" in summary_field.value
|
assert "15 pending" in summary_field.value
|
||||||
|
|
||||||
|
# Verify pagination view was created
|
||||||
|
from commands.transactions.management import TransactionPaginationView
|
||||||
|
view = embed_call.kwargs.get('view')
|
||||||
|
assert view is not None
|
||||||
|
assert isinstance(view, TransactionPaginationView)
|
||||||
|
assert len(view.all_transactions) == 15
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_concurrent_command_execution(self, commands_cog):
|
async def test_concurrent_command_execution(self, commands_cog):
|
||||||
"""Test that commands can handle concurrent execution."""
|
"""Test that commands can handle concurrent execution."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Create multiple mock interactions
|
# Create multiple mock interactions
|
||||||
interactions = []
|
interactions = []
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
mock_interaction = AsyncMock()
|
mock_interaction = AsyncMock()
|
||||||
mock_interaction.user.id = 258104532423147520 + i
|
mock_interaction.user.id = 258104532423147520 + i
|
||||||
interactions.append(mock_interaction)
|
interactions.append(mock_interaction)
|
||||||
|
|
||||||
mock_team = Team.from_api_data({
|
mock_team = Team.from_api_data({
|
||||||
'id': 499,
|
'id': 499,
|
||||||
'abbrev': 'WV',
|
'abbrev': 'WV',
|
||||||
@ -538,22 +664,22 @@ class TestTransactionCommandsIntegration:
|
|||||||
'lname': 'West Virginia Black Bears',
|
'lname': 'West Virginia Black Bears',
|
||||||
'season': 12
|
'season': 12
|
||||||
})
|
})
|
||||||
|
|
||||||
with patch('commands.transactions.management.team_service') as mock_team_service:
|
with patch('utils.team_utils.team_service') as mock_team_service:
|
||||||
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
|
||||||
|
|
||||||
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
|
||||||
mock_tx_service.get_pending_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_pending_transactions = AsyncMock(return_value=[])
|
||||||
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=[])
|
||||||
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Execute commands concurrently
|
# Execute commands concurrently
|
||||||
tasks = [commands_cog.my_moves.callback(commands_cog, interaction) for interaction in interactions]
|
tasks = [commands_cog.my_moves.callback(commands_cog, interaction) for interaction in interactions]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
# All should complete successfully
|
# All should complete successfully
|
||||||
assert len([r for r in results if not isinstance(r, Exception)]) == 5
|
assert len([r for r in results if not isinstance(r, Exception)]) == 5
|
||||||
|
|
||||||
# All interactions should have received responses
|
# All interactions should have received responses
|
||||||
for interaction in interactions:
|
for interaction in interactions:
|
||||||
interaction.followup.send.assert_called_once()
|
interaction.followup.send.assert_called_once()
|
||||||
Loading…
Reference in New Issue
Block a user